Websocket
背景
最近遇到一个现场问题,客户反馈产品页面提示连接错误,一查是ws不能链接,借此复习下ws的内容。毕竟之前写ws功能是好几年前了。温故知新。
https://ably.com/topic/websockets
https://www.rfc-editor.org/rfc/rfc6455
https://websockets.spec.whatwg.org/#websocket-opening-handshake
最近遇到一个现场问题,客户反馈产品页面提示连接错误,一查是ws不能链接,借此复习下ws的内容。毕竟之前写ws功能是好几年前了。温故知新。
https://ably.com/topic/websockets
https://www.rfc-editor.org/rfc/rfc6455
https://websockets.spec.whatwg.org/#websocket-opening-handshake
最近处理一个现场故障,现象是系统用一段时间就会变得很慢,clickhouse会重启,etcd发生长时间起不来,业务日志有代码报错。
总结下来大体发现如下几个问题:
业务代码问题先给到业务方进行排查,我们先看clickhouse和etcd问题
因为对于基础组件都没开系统日志,所以只能从观察系统的指标开始,通过netdata,发现clickhouse会不断的打到峰值
所以先从clickhouse查起,因为没有开日志,所以就从clickhoue提供的系统表开始查起
我们找了下官方资料,先锁定了如下几张表
1 | -- 查看可疑的sql |
最近处理一个现场,80端口正常开放,但是从外部curl无法访问,pod内部是没问题的
初步判断时k8s内部的转发出了问题
https://www.tkng.io/services/clusterip/dataplane/iptables/
https://kubernetes.io/blog/2022/09/07/iptables-chains-not-api/
https://dustinspecker.com/posts/iptables-how-kubernetes-services-direct-traffic-to-pods/
https://medium.com/swlh/manage-iptables-firewall-for-docker-kubernetes-daa5870aca4d
https://livebook.manning.com/concept/kubernetes/iptable
https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/
https://jimmysong.io/en/blog/understanding-iptables/
https://blog.csdn.net/cloudvtech/article/details/79728920
最近看了本书《价值》,高瓴资本的张磊写的,人和其所在的行业虽然我都不熟,但是当时在书店随便拿了本看看。
这是目前唯一一本从序,开始就抓住我眼球的书,当然我看的书不多就是了,我估计是因为刚好最近有些焦虑和颓废的原因吧,这本书仿佛是孙悟空派来叫醒我的。
坚持长期主义,这是一条越走越不孤独的路,这是一条越走越行稳致远的路。
长期主义-把时间和信念投入到能够长期产生价值的事件中,尽力学习最有效率的思维方式和行为标准,遵循第一性原理,永远探求真理。
长期主义与个人而言,是一种清醒,帮助人们建立理性的认知框架,不受短期诱惑和繁杂噪声的影响。
对企业而言,是一种格局,帮助企业拒绝狭隘的零和游戏,在不断创新、不断创造的价值历程中,重塑企业的动态护城河。
一个人的知识、能力和价值观,才是深藏于心,并真正属于自己的”三把火“
方法和策略能战胜市场,但对长期主义的信仰却能够赢得未来。
发表自己真实的想法和意见,不去争论谁是对的,而是去争论什么是对的。
更多的研究是为了更少的决策,更久的研究是为了更准的决策。
很多时候迷恋数据是为了拿数据当挡箭牌,抵挡因为懒惰带来的错误,从而把责任怪到数据上。
正确的理解应该是:精确的数据无法代替大方向上的判断,战术上的勤奋不能弥补战略上的懒惰
克莱顿·克里斯坦森关于创新的诠释:创新不在于技术进步,更不在于科学发现,而在于对市场变迁的主动响应。
世界上只有一条护城河,就是企业家门不断创新,不断疯狂地创造长期价值。
克莱顿·克里斯坦森《创新者的窘境》将创新定位两类:
不断完善和改进现有产品,通过精耕细作满足更挑剔的需求。但是偶然的非主观的主义则无法融入企业创新流程。
追求的根本性的改变,从底层出发改变现有的技术发展路径和思维方式,创造出区别于现有主流市场的全新产品合服务,这种创新可能会对原油的护城河降维打击。
较长的员工成长的还得益于足够扁平的组织机制。扁平的组织机制能够保证更多有价值的想法随时听到,“多汇报,少请示”。特别是对于新人,即使反馈,从而得到反馈,能得到足够的尊重。
团队内营造更好的倾听反馈和给予关怀的氛围。
自我驱动能力。
员工的成长有两方面:
对于有潜质、价值观一致的员工,我们奉行长期主义,不着急,慢慢来,给予其充分的成长时间。
全力打造学习型组织。倡导“传、帮、带”文化与跨团队的交流和分享,创造一切条件促进内部学习。“传、帮、带”不是为了消灭个性。
分享周?跨团队分享机制。
团队协作,复盘和纠错机制。
第二曲线理论。
《七律·和柳亚子先生》
问题也是源于一个客户现场问题。
经查原因如下:
You:
什么是OSIV
ChatGPT:
我不确定你指的是什么是 “OSIV”。 请问您能否提供更多上下文或信息,以便我更好地回答您的问题?
You:
spring里的
ChatGPT:
在Spring中,”OSIV” 是 “Open Session In View” 的缩写,它是一种用于在Web应用程序中处理Hibernate(或其他ORM框架)延迟加载异常的技术。
具体而言,当在Web应用程序中使用Hibernate时,通常会在每个HTTP请求上打开一个Hibernate会话,以便加载数据并填充视图。 在没有OSIV的情况下,当在视图中使用延迟加载属性时,可能会发生”LazyInitializationException”异常。
通过启用OSIV,Spring会在Web请求处理期间保持Hibernate会话打开状态,从而避免延迟加载异常。但是,这种技术也可能导致一些不良影响,例如过度加载和缓存问题,因此使用时应谨慎。
You:
优点缺点分别是什么
ChatGPT:
以下是 “Open Session In View”(OSIV)的优点和缺点:
优点:
避免延迟加载异常:OSIV 可以确保在Web请求处理期间Hibernate会话一直处于打开状态,从而避免了在视图中使用延迟加载属性时可能出现的 “LazyInitializationException” 异常。
简化开发:启用OSIV可以简化开发过程,因为开发人员不需要显式管理Hibernate会话的打开和关闭。这使得代码更加简洁易懂。
提高性能:由于会话不需要在每次需要数据时重新打开和关闭,因此启用OSIV可能会提高应用程序的性能。
缺点:
过度加载:启用OSIV可能会导致过度加载,即在应用程序的生命周期中始终保持数据库连接处于打开状态,这可能会导致性能下降和资源消耗过高。
缓存问题:在使用缓存时,启用OSIV可能会导致缓存中的数据变得过时,从而导致数据不一致性。
难以调试:由于会话在整个请求周期中处于打开状态,因此在调试时可能很难确定哪个代码块打开和关闭了会话,从而导致问题的难以定位。
You:
缓存问题具体是什么意思
ChatGPT:
启用 “Open Session In View”(OSIV)可能会导致缓存问题,这是因为Hibernate具有一个二级缓存机制,它可以缓存从数据库中检索的实体对象,以避免在每次需要访问它们时都要进行数据库查询。而当启用OSIV时,Hibernate会话保持打开状态,从而可能会导致缓存中的数据变得过时。
具体而言,如果在请求期间Hibernate会话中的某些实体对象被更新或删除,但是由于启用了OSIV,Hibernate会话没有关闭,那么缓存中仍然包含过时的实体对象。如果在同一请求中需要访问这些实体对象,那么就会使用缓存中的过时数据,这可能会导致数据不一致性的问题。
为了避免这种情况,通常建议在处理请求后立即关闭Hibernate会话,以确保缓存中的数据始终与数据库中的数据保持同步。
You:
那就是不建议打开OSIV呗
ChatGPT:
是否启用 “Open Session In View”(OSIV)通常取决于应用程序的具体需求和开发人员的偏好。虽然启用OSIV可以简化开发过程并避免延迟加载异常,但也可能会导致过度加载和缓存问题等不良影响。
因此,建议在决定是否使用OSIV时需要考虑应用程序的实际需求。对于一些简单的应用程序,可能不需要使用OSIV,而对于一些复杂的应用程序,启用OSIV可能会带来实际的好处。
同时,如果您决定使用OSIV,请注意使用它的潜在风险,并遵循最佳实践,例如在请求结束后立即关闭Hibernate会话以避免缓存问题。
You:
如果开启了OSIV,怎么避免这类问题呢
ChatGPT:
如果您决定启用 “Open Session In View”(OSIV),可以采取以下措施来避免缓存问题:
只缓存只读数据:在使用缓存时,只缓存只读的实体对象,而不是缓存可以进行修改的实体对象。这可以避免缓存中的数据被过时数据污染。
显式刷新缓存:在每个请求的结束处,显式地刷新Hibernate的二级缓存。这可以确保缓存中的数据与数据库中的数据保持一致。
使用版本控制:在实体类中使用版本控制可以帮助确保在多个事务中不会出现数据不一致的问题。Hibernate会在更新实体时使用版本控制,并在并发修改发生时抛出异常。
禁用缓存:如果缓存问题太棘手,或者您无法控制缓存中的数据,那么可以考虑完全禁用Hibernate的二级缓存。这虽然会降低性能,但可以避免缓存问题。
总之,如果您决定启用OSIV,则应该注意缓存问题,并采取适当的措施来避免数据不一致性。同时,建议在开发过程中进行充分测试,以确保应用程序在使用OSIV时不会出现问题。
You:
怎么刷新缓存
ChatGPT:
要刷新Hibernate的二级缓存,可以使用以下方法之一:
SessionFactory
的 getCache()
方法获取二级缓存,然后调用 evictAllRegions()
方法刷新缓存,示例如下:1 | SessionFactory sessionFactory = ... // 获取SessionFactory实例 |
hibernate.cache.use_second_level_cache
和 hibernate.cache.use_query_cache
属性为 true
,并在事务提交时自动刷新缓存。示例如下:1 | <property name="hibernate.cache.use_second_level_cache">true</property> |
此外,还可以通过在实体类上使用 @Cache
注解来控制实体类的缓存策略,例如:
1 | @Entity |
在这个例子中,实体类 User
将被缓存,并使用 READ_WRITE
缓存策略。如果在更新 User
对象时使用了 Hibernate 的缓存,则会自动刷新该对象的缓存。
总之,要刷新Hibernate的二级缓存,可以手动刷新缓存或在事务提交时自动刷新缓存,还可以通过使用 @Cache
注解来控制实体类的缓存策略。
You:
怎么使用使用版本控制
ChatGPT:
在Hibernate中使用版本控制可以帮助确保在多个事务中不会出现数据不一致的问题。Hibernate通过使用版本号来实现版本控制,每个实体类都必须定义一个版本号属性。当更新实体时,Hibernate会自动更新版本号,并在并发修改发生时抛出异常。以下是如何使用版本控制的步骤:
@Version
注解,示例如下:1 | @Entity |
StaleObjectStateException
异常,示例如下:1 | User user = session.get(User.class, 1L); |
如果在此之前,有另一个事务更新了同一行记录,那么在第二个事务中更新实体时,Hibernate会抛出 StaleObjectStateException
异常,示例如下:
1 | User user = session.get(User.class, 1L); |
1 | User user = session.get(User.class, 1L); |
总之,在Hibernate中使用版本控制可以帮助确保在多个事务中不会出现数据不一致的问题。可以在实体类中定义版本号属性,并在更新实体时处理并发修改异常。
You:
请给一个完整的版本冲突的代码实现
ChatGPT:
以下是一个简单的版本冲突的代码实现,其中使用乐观锁处理并发修改异常:
1 | @Entity |
在上面的代码中,首先获取要更新的实体,并将其名称设置为新名称。然后调用 session.update()
方法将实体标记为“脏”并更新到缓存中。最后,使用 session.flush()
方法将更改保存到数据库。如果在此之前有另一个事务更新了同一行记录,则在当前事务中调用 session.flush()
方法时,Hibernate会抛出 OptimisticLockException
异常。可以在异常处理程序中处理并发修改异常,并根据需要回滚事务。
You:
怎么回滚事务
ChatGPT:
在Java中,可以使用try-catch语句块和异常处理程序来回滚事务。如果在事务中出现异常,则可以在异常处理程序中调用 rollback()
方法来回滚事务。以下是一个简单的示例:
1 | // 在事务中执行数据库操作 |
在上面的代码中,使用了 Spring Framework 的事务注解 @Transactional
来标记方法作为事务。在方法中,执行数据库操作,然后使用try-catch语句块捕获异常。如果出现异常,则调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
方法将事务标记为回滚状态,并将异常抛出给调用者。这将导致 Spring 回滚事务并将异常传播给调用者。
You:
怎么禁用二级缓存
ChatGPT:
禁用二级缓存需要在Hibernate的配置文件中进行设置。以下是禁用Hibernate二级缓存的配置示例:
1 | <hibernate-configuration> |
在上面的配置中,设置了 hibernate.cache.use_second_level_cache
属性为 false
,表示禁用了Hibernate的二级缓存。
需要注意的是,如果应用程序使用了Hibernate的查询缓存,仍然需要将 hibernate.cache.use_query_cache
属性设置为 true
,否则查询缓存也会被禁用。
另外,如果使用Spring框架进行数据访问,可以在 application.properties
文件中配置 spring.jpa.properties.hibernate.cache.use_second_level_cache
属性来禁用Hibernate二级缓存:
1 | spring.jpa.properties.hibernate.cache.use_second_level_cache=false |
You:
OSIV什么情况下才会关闭session
ChatGPT:
在开启 Open Session In View (OSIV) 模式的情况下,session 会在请求结束时自动关闭。具体来说,一般情况下,在控制器方法执行完毕后,Spring 框架会调用 DispatcherServlet
的 processDispatchResult
方法,该方法会在返回响应之前关闭 Hibernate Session
。这样可以确保在整个请求处理过程中都可以访问数据库并且避免了一些潜在的问题,比如:延迟加载异常等。
但是,需要注意的是,在使用某些数据访问方式时(如 JdbcTemplate
),并不会自动开启 Hibernate Session,因此也就不会自动关闭。在这种情况下,需要手动开启 Session,并在使用完毕后手动关闭 Session,以避免潜在的连接泄漏问题。
总之,在使用 Open Session In View 模式时,要注意确保 session 在请求结束时被正确地关闭,以免出现连接泄漏等问题。
https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot
https://medium.com/@rafaelralf90/open-session-in-view-is-evil-fd9a21645f8e
先得翻墙,有境外的vps。
能接收国外的短信,我用的sms-activate
如果境外的也被封禁,在服务器上安装Cloudflare Warp,让Warp接管ChatGPT域名的访问流量即可。亲测有效。
需要chatgpt的api token
输出比较慢,估计是一次性把完整结果给出来的原因。可写代码、注释、找bug、重构分析代码等
可写代码、注释、找bug、重构分析代码等
https://www.cursor.so/
没啥好说的,就是把文档解析后,可以通过提问的方式从文档里找答案
https://chatdoc.com/
需要chatgpt的api token
可以与google搜索无缝集成,且可以直接下载结构为md文档
https://github.com/josStorer/chatGPTBox
各种基于chatgpt的工具,但是这个最原始的反而是我最喜欢用的,简洁明了
https://chat.openai.com/chat
事件缘起于遇到的一个现场问题
先问原因:
如果是主机不正常关机,很可能是 操作系统的文件系统损害
如果是docker的某种操作,那就修改docker
如果以上都没问题,再考虑修复clickhouse
如果是主机问题,还需要用户解决呢;如果是二、三记录场景,看看如何避免。
不论哪个,先询问情况吧。可以用uptime、docker ps等看看启动时间,然后对照日志时间,看看是否对的上
16:16
估计是主机非正常关机,linux系统很稳定,很少出现文件系统损害。
如果是这样,需要开远程,慎重验证、修复。并向用户澄清,有丢失数据的风险(毕竟文件系统坏了),在首肯之后再操作
如果挂载了特殊的 硬件,如:RAID、NAS,由用户的主机管理员解决。(坚决禁止NAS)
非正常关机了?或者升级k8s重启?
merge过程分多阶段:
生成新文件夹
将待合并的多个文件夹,读取合并到新文件夹
删除旧文件夹
修改相关的系统表(分区表等)
这个问题,可以到github看clickhouse源代码,然后猜测问题点
df -T -x tmpfs
看来文件夹都在,只是文件系统记录的 状态 和 落盘 不一致了
文件系统需要卸载,所以先要看 有哪些应用在使用 /opt/local-path-provisioner,将k8s相关的服务都关闭吧
!!确定相关业务已经关闭,然后一个个执行
umount /dev/sdb1
xfs_repair /dev/sdb1
mount /dev/sdb1
lsblk -f
17:39
parted -l 可以查看未挂载的文件系统类型,以及那些分区尚未格式化
会不会还有程序占用着相关文件?
lsof +d /opt/local-path-xxxxxprovoider
后边的路径就是挂载路径
刚还试了
没有lsof命令
ps -elf 看看是否有可疑进程,如果关闭k8s,应该没有几个进程在了
pstree命令执行一下,我看看继承顺序
另一个经常出的日志占用内存问题:删除文件后,进程依然持有文件描述符,内容将输出到内存
以后现场会发生的问题:经常出现断网——造成终端停止、里边的长时间命令就会失败
建议方法:启动一个tmux,启动命令后,脱离终端,等过一段时间再attach上去
https://bbs.qunyingkeji.com/2052/
又是一篇陈皓老师的文章https://coolshell.cn/articles/10217.html
还是那句不要脸的话,真的是深有同感,特别是里面关于效率和产品那块的表达,真是我经常在心里想的和同事们聊的,不过也仅限于聊聊。再往上就会触发各种不满了。
认为加班是公司的核心竞争力,或是超越对手的手段,是一种相当 Ridiculous 的想法。
产品的发展不是短跑,而是长跑,甚至更像是登山,登山比的不是快,而比的是策略,比的是意志,目的是登顶。
工作狂往往不得要领。他们花大把大把的时间去解决问题,他们以为能靠蛮力来弥补思维上的惰性,其结果就是折腾出一堆粗糙无用的解决方案
很多人不知道什么叫效率,他们以为效率就是:单位时间单位人数下干更多的活。这是错的!效率不是比谁干的活多,而是比谁干得活有更大的价值。效率的物理公式是:有用功/总功。换句话说,效率就是:单位时间和人数产生的价值。
有用功的来源不是拼命做需求,而是砍需求。
效率不是每个团队各自的效率,而是整个团队对整个产品负责的共同使命,这样才会现整体的效率。没有整体的效率,只有个体的效率,最终也等于没有效率。
T-Shirt Size Estimation
很多时候说是产品其实还是按照项目在做,就会有个人认为特别大的问题,一定要追求大而全,蛮力堆功能,宁可错杀一千不能放过一百。不然就拿不下某块领域的客户,但是真的只能这么做吗?肯定不是的,因为这么做肯定是活不长的,咱们干技术的就知道,啥都要就是一种幻想,现实往往是啥都不精,然后自己把自己给捆紧了,啥也不敢做也不能做了。
最后只有两条路:
1.就这么一直不停的缝缝补补、纠纠缠缠根本没时间和精力与时俱进。
2.挣脱出来重新来一遍。但是又有多少资源和时间能重来呢?
读陈皓老师的https://coolshell.cn/articles/17497.html有感。
https://brendansterne.com/2013/07/11/do-the-right-thing-wait-to-get-fired/
https://news.ycombinator.com/item?id=6029823
除非你是算命先生,长期的商业计划是种幻想。有太多的事实证明那是超出你的掌控的:市场环境、对手、顾客、经济等等。做计划让你觉得一切尽在掌握但实际上你没有。
当你把计划变成猜测时,就等于进入一个危险的境地。做计划就是在用过去推导未来,等于给你戴上了眼罩。
感想:你有职业规划吗?如果你有的话,那么你就一定就错了。职业规划是一件很扯淡的事情。我和一些高手都交流过,其实这些人在当初都并不有什么职业规划的,要说有的话,也就是想把技术搞透搞精。这些人在一开始从来没有想过要当个什么经理或是什么架构师之类的东西,这些人就是对技术有非常大的热情,把身边的那些看得见够得着的事情做到好好地,并且保持不持续强大的好奇心努力地学习自己不懂的东西。一个坚定不移的决定和意志力会比任何的计划和职业规划都重要。你问问自己,想不想当程序员,能不能一辈子都当一个程序员,能不能写程序写一辈子?(关于做一辈子程序员这个事,大家可以看看我的新浪微博 ——没哪个行业能像计算机行业这么活跃、刺激和有趣了。不仅是新兴工业革命的主力,又渗入到所有的行业中,干一辈子值了。//@_你亲爱的偏执狂: 程序员首先是工程师,Professional,就跟律师,医生一样,给大家解决问题;但是另一面呢,又是艺术家,创造新奇好玩的东西。这样的职业做一辈子有什么问题?)
http://www.effectiveengineer.com/blog/what-makes-a-good-engineering-culture
https://www.quora.com/Software-Engineering/What-makes-a-good-engineering-culture/answer/Edmond-Lau?share=1
维护高质量的代码库可以提高整个工程团队的生产力。更简洁的代码更容易推理、更快地开发、更易于更改并且更不容易受到错误的影响。一个健康的代码审查过程使这成为可能。
建立一个及时的代码审查流程,无论是提交前还是提交后,都可以在几个方面提高代码质量。首先,知道有人会审查你的代码并且提交写得不好的代码可能会让你的队友失望的同行压力是对骇客、不可维护或未经测试的代码的强大威慑。其次,代码审查为代码审查者和作者提供了相互学习以编写更好的代码的机会。
如果工程团队的其他成员可以轻松访问代码审查,那么审查还带来以下好处:a)增加及时审查代码的责任,b)允许团队成员 - 特别是新成员 - 进行建模其他人的良好代码审查,以及 c) 加速传播最佳编码实践。
敏捷团队没有时间花在代码审查上的反驳忽略了很容易从编写不佳的代码中积累的技术债务。Ooyala 在其早期的启动阶段,用于优化以尽可能多地推出功能,而无需进行代码审查;结果是,虽然最初的产品可能更快地推向市场,但最终的代码修改起来很痛苦,我们花了一年多的时间重写脆弱的代码以消除技术债务。
就其规模而言,Google 会对所有代码进行预先提交的代码审查,但较小的团队不需要如此全面或严格,并且并非所有代码都需要以同样严格的方式审查。Ooyala 后来在我在那里时通过电子邮件对核心或有风险的更改进行了提交后审查。在 Quora,我们在Phabricator中进行了所有代码审查,主要是提交后,并对模型或控制器代码和视图代码应用不同的标准;对于敏感代码或新工程师的代码,我们要么进行预提交审查,要么尝试在提交代码后的几个小时内对其进行审查。
同行之间的尊重构成了任何类型的开放式沟通的基础。一个人们乐于挑战彼此想法的地方是一个通过辩论形成合理想法的地方。人们容易被冒犯的地方是关键反馈被隐瞒的地方。
虽然个人精通代码库或基础设施的各个部分是很自然的,但没有人应该觉得他们拥有或是任何一个部分的唯一维护者。
在组织上,共享代码所有权提供了三个好处。首先,保持总线因子大于 1 可以减轻维护者的压力,并降低团队在维护者离开时的风险。这也使那个人很难无忧无虑地休假。
其次,共享所有权使在特定领域不深入的工程师能够提供新的见解。它使工程师摆脱了他们被困在某些项目上的感觉,并鼓励他们从事各种项目,这有助于保持工作的趣味性并促进员工的学习和积极性。从长远来看,它降低了一些工程师感到停滞不前并决定离开的组织风险。
第三,共享所有权还为在需要更快地完成战略目标时让多个团队成员蜂拥而至(来自敏捷开发的一种技术)共同解决高优先级问题奠定了基础。对于孤立的所有权,负担通常落在一两个人身上。
单元测试覆盖率和某种程度的集成测试覆盖率是管理一大群人的大型代码库而不不断破坏构建或产品的唯一可扩展方式。自动化测试为提高代码质量所需的大规模重构提供了信心和有意义的保护。在缺乏严格的自动化测试的情况下,工程团队或外包测试团队进行手动测试所需的时间很容易变得令人望而却步,并且很容易陷入一种害怕改进一段代码的文化,因为它可能会破坏。
强大自动化测试将验证责任转移到原作者身上,减少不熟悉的人的投入。
如果没有足够的工程经验,很难识别出要构建的正确抽象。如果没有其他聪明人来挑战你的想法并推动你走向简单,很容易陷入构建复杂事物的陷阱。
硅谷有句谚语,由史蒂夫·乔布斯(Steve Jobs)创造,“A 球员雇佣 A 球员。B 玩家雇佣 C 玩家。” 专注于招聘和聘用合适的人是困难的,但对于有效发展工程组织至关重要
因为低质量代码和团队中较弱的工程师带来的技术债务最终会伤害团队和产品。