我的名字叫Danil Mukhametzyanov,我在Badoo从事后端开发工作已经七年了。在这段时间里,我设法创建和更改了大量代码。是如此之大,以至于有一天一位经理来到我身边说:“配额已经结束了。要添加某些内容,您需要删除一些内容。”
好吧,这只是个玩笑-他没那么说。真遗憾!在公司的整个生存过程中,Badoo已经积累了超过550万行逻辑业务代码,不包括空白行和右括号。
数量本身并不那么可怕:他撒谎,不要求食物。但是两三年前,我开始注意到我阅读的频率越来越高,并试图找出在生产环境中实际上不起作用的代码。也就是说,实际上已经死了。
这种趋势不仅被我注意到。Badoo意识到我们高薪的工程师一直在浪费时间在死代码上。
我在Badoo PHP Meetup#4上做了这个演讲
无效代码从何而来?
我们开始寻找问题的原因。将它们分为两类:
- 过程-由于发展而产生的过程;
- 历史-旧版代码。
首先,我们决定分解过程源,以防止出现新问题。
A / B测试
Badoo四年前开始积极使用A / B测试。现在,我们大约有200个测试在不断运行,所有产品功能都通过此过程进行。
结果,四年中累计完成了约2000项测试,并且这个数字还在不断增长。她使我们感到害怕,每个测试都是一段死代码,不再执行且根本不需要。
解决问题的方法很快:我们开始自动创建票证,以在完成A / B测试后剪切代码。
票证的示例
但是人为因素会定期触发。一遍又一遍,我们发现测试代码可以继续运行,但是没有人想到它并完成了测试。
然后有一个严格的框架:每个测试必须有一个结束日期。如果经理忘记读取测试结果,他将自动停止并关闭。而且,正如我已经提到的,在维护功能逻辑的原始版本的同时,会自动创建一个票证以将其删除。
借助这种简单的机制,我们摆脱了繁重的工作。
客户的多样性
我们公司支持多个品牌,但服务器是其中之一。每个品牌在三个平台上都有代表:Web,iOS和Android。在iOS和Android上,我们有一个每周的开发周期:每周一次,同时进行更新,我们在每个平台上收到该应用程序的新版本。
很容易猜到,使用这种方法,一个月内我们需要支持大约十二种新版本。用户流量在它们之间分布不均:用户逐渐从一种版本切换到另一种版本。某些较旧的版本具有流量,但流量很小,难以维护。很难,没有用。
因此,我们开始计算我们要支持的版本数。客户端有两个限制:软限制和硬限制。
当达到软限制时(当已经发布了三个或四个新版本,并且该应用程序仍未更新时),用户会看到一个屏幕,上面有警告说他的版本已过期。当达到硬限制(根据应用程序和品牌的不同,大约为10-20个“缺失”版本)时,我们只需删除该选项即可跳过此屏幕。它变得阻塞:您无法将其与该应用程序一起使用。
硬限制的屏幕
在这种情况下,继续处理来自客户端的请求是没有用的-他只会看到屏幕。
但是在这里,与A / B测试一样,产生了细微差别。客户开发人员也是人。他们使用新技术,操作系统芯片-不久之后,下一版本的操作系统不再支持该应用程序版本。但是,服务器继续受苦,因为它必须继续处理这些请求。
当Windows Phone的支持终止时,我们提出了一个单独的解决方案。我们准备了一个屏幕,通知用户:“我们非常爱您!你真酷!但是,您可以开始使用其他平台了吗?新的炫酷功能将向您提供,但是在这里我们无能为力。” 通常,我们提供一个Web平台作为替代平台,该平台始终可用。
通过这种简单的机制,我们限制了服务器支持的客户端版本的数量:所有品牌,所有平台的大约100个不同版本。
功能标记
但是,通过禁用对较旧平台的支持,我们无法完全理解是否有可能完全切掉它们使用的代码。还是保留用于较旧OS版本的平台继续使用相同的功能?
问题在于我们的API不是建立在版本控制的部分上,而是建立在功能标志的使用上。我们是如何做到这一点的,您可以从这份报告中找到。
我们有两种类型的功能标志。我将通过示例向您介绍它们。
次要功能
客户端对服务器说:“你好,是我。我支持照片帖子。” 服务器看着它并回答:“太好了,支持!现在我知道了,它将向您发送照片消息。” 此处的关键功能是服务器无法以任何方式影响客户端-它仅接受来自客户端的消息并被迫侦听。
我们称这些标志为次要功能。目前我们有600多个
,使用这些标志的缺点是什么?周期性地,有繁重的功能无法仅从客户端获得,您也希望从服务器端进行控制。为此,我们引入了其他类型的标志。
应用功能
相同的客户端,相同的服务器。客户说:“服务器,我学会了支持视频流。打开它吗?服务器回答:“谢谢,我会记住这一点。”他补充说:“太好了。让我们向心爱的用户展示此功能,他会很高兴的。”或者:“好吧,但我们还不包括在内。”
我们称这些功能为应用程序功能。它们比较重,因此我们有较少的数量,但仍然足够:超过300个。
因此,用户从客户端的一个版本转移到另一个版本。所有活动版本的应用程序都开始支持某种标志。或者,相反,不支持。目前尚不清楚如何控制它:100个客户端版本,900个标志!为了解决这个问题,我们构建了一个仪表板。
其上的红色方块表示该平台的所有版本均不支持此功能。绿色-此平台的所有版本均支持此标志。如果该标志可以关闭和打开,则它将定期闪烁。我们可以看到在哪个版本中会发生什么。
仪表板屏幕
在此界面中,我们开始创建用于剪切功能的任务。应该注意的是,并不是每行中的所有红色或绿色单元都需要填写。有些标志只能在一个平台上运行。有些标志只能填写一个品牌。
自动化过程不是很方便,但是原则上没有必要-您只需要设置一个任务并定期查看仪表板即可。在第一个迭代中,我们设法剪切了200多个标志。这几乎是我们使用的标志的四分之一!
这是过程源结束的地方。它们的出现是我们开发流程的结果,并且我们已经成功地将其工作整合到了此过程中。
遗留代码怎么办
我们已停止在过程源中出现新问题。我们面临着一个难题:如何解决这些年来积累的遗留代码?我们从工程学的角度来研究该解决方案,也就是说,我们决定使一切自动化。但是尚不清楚如何找到未使用的代码。他躲在自己舒适的小世界里:以任何方式都没有被召唤,也没有让任何人知道他自己。
我们必须从另一侧出发:获取我们拥有的所有代码,收集有关确切执行了哪些代码的信息,然后进行反转。
然后,我们将其放在一起并以最低限度的级别在文件上实施。这样,我们可以通过运行适当的UNIX命令轻松地从存储库中获取文件列表。
剩下的只是收集生产中使用的文件列表。这非常简单:对于关闭时的每个请求,调用相应的PHP函数。我们在这里所做的唯一优化是开始请求OPCache而不是请求每个请求。否则,数据量将非常大。
结果,我们发现了许多有趣的工件。但是通过更深入的分析,我们意识到我们缺少了未使用的方法:它们的数量相差三到七倍。
事实证明,仅出于一个常量或一对方法的原因,可以加载,执行,编译文件。其他一切都没用,只能躺在这无底的海洋中。
汇总方法列表
但是,结果很快就可以收集完整的方法列表。我们只是拿了尼基塔·波波夫(Nikita Popov)的解析器,将他的资料库喂给他,并获得了代码中所有的东西。
问题仍然存在:如何组装生产中正在播放的内容?我们对生产感兴趣,因为测试可以覆盖我们根本不需要的东西。三思而后行,我们选择了XHProf。它已经在生产中用于部分查询,因此,我们将配置文件样本存储在数据库中。仅去这些数据库,解析生成的快照并获取文件列表就足够了。
XHProf的缺点
我们在另一个没有启动XHProf但非常需要的集群上重复了此过程。这是一个用于运行后台脚本和异步处理的集群,这对于高负载非常重要,它运行大量逻辑。
然后,我们确保XHProf对我们不方便。
- 它需要更改PHP代码。您需要插入跟踪开始代码,完成跟踪,获取收集的数据,并将其写入文件。毕竟,这是一个探查器,但是我们有产品,也就是说,有很多要求,您还需要考虑采样。在我们的案例中,大量具有不同入口点的集群使情况更加恶化。
- . . , OPCache. : XHProf, . , core- .
- . . XHProf . ( XHProf): CPU, , . , , . - XHProf aggregator ( XHProf Live Profiler, open-source) , , , . , : «, , », CPU , , Live Profiler . , , .
- XHProf. , . . , . : , ( , youROCK,这不是lsd所必需的,但是在上面维护一个包装器更为方便。修补XHProf不是我们想要做的,因为它是一个相当大的探查器(如果我们无意间破坏了某些内容,该怎么办?)。
还有一个主意-将某些名称空间(例如,从作曲家中排除的供应商名称空间)排除在生产环境中,因为它们是无用的,因为它们是无用的:我们不会重构供应商软件包并从中删去不必要的代码。
解决方案要求
我们再次聚在一起,研究存在的解决方案。他们制定了最终的需求清单。
第一:开销最小。对我们而言,XHProf是门槛:仅此而已。
其次,我们不想更改PHP代码。
第三,我们希望该解决方案可以在FPM和CLI中的任何地方使用。
第四,我们要处理货叉。它们已在云服务器上的CLI中积极使用。我不想在PHP中为它们制定特定的逻辑。
第五:开箱即用。实际上,这是出于不更改PHP代码的要求。下面我将解释为什么我们需要采样。
第六和最后:从代码强制的能力。当一切自动运行时,我们会喜欢它,但有时手动启动,调整和外观会更方便。我们需要直接从代码中启用和禁用所有功能的能力,而不是通过PHP模块更通用的机制(通过设置来设置包含概率)的随机决定。
funcmap如何工作
结果,我们有了一个称为funcmap的解决方案。
Funcmap本质上是一个PHP扩展。用PHP术语来说,这是一个PHP模块。为了了解它是如何工作的,让我们看一下PHP流程和PHP模块是如何工作的。
因此,您开始一个过程。 PHP使构建模块时可以订阅钩子成为可能。该过程开始,启动GINIT(全局初始化)挂钩,您可以在其中初始化全局参数。然后初始化模块。可以在其中创建和分配常量,但是只能为特定模块创建常量,而不能为请求创建常量,否则您将无法自拔。
然后,用户请求进入,调用RINIT(请求初始化)挂钩。请求完成后,将关闭它,并最终关闭模块:MSHUTDOWN和GSHUTDOWN。一切都是合乎逻辑的。
如果我们谈论的是FPM,那么每个用户请求都会到达一个已经存在的工作程序中。基本上,RINIT和RSHUTDOWN只是绕圈工作,直到FPM决定该工人已经过时,是时候射击他并创建一个新工人。如果我们谈论的是CLI,那只是一个线性过程。一切都会被调用一次。
funcmap的工作方式
在这个集合之外,我们对两个钩子感兴趣。第一个是RINIT。我们开始设置数据收集标志:这是一种随机抽样,用于采样数据。如果可行,我们将处理此请求:我们收集了针对该函数和方法调用的统计信息。如果它不起作用,则该请求未被处理。
下一步是创建哈希表(如果不存在)。哈希表由PHP本身内部提供。无需在这里发明任何东西-只需将其使用即可。
接下来,我们初始化计时器。我将在下面谈论他,现在,请记住他是重要,重要和需要的。
第二个挂钩是MSHUTDOWN... 我想指出的是它是MSHUTDOWN,而不是RSHUTDOWN。我们不想为每个请求制定出一些东西-我们对整个工作人员都很感兴趣。在MSHUTDOWN上,我们获取哈希表,遍历该表并编写一个文件(有什么比旧的文件更可靠,更方便,更通用?)。
哈希表非常简单地由相同的PHP挂钩zend_execute_ex填充,每次调用用户定义的函数时都会调用该挂钩。记录包含其他参数,通过这些参数您可以了解它的功能类型,名称和类。我们接受它,读取名称,将其写入哈希表,然后调用默认钩子。
该钩子不编写内联函数。如果要覆盖内置函数,则有一个单独的功能称为zend_execute_internal。
组态
如何在不更改PHP代码的情况下进行配置?设置非常简单:
- 已启用:是否启用。
- 我们正在写入的文件。当不同的PHP进程同时写入同一文件时,有一个pid占位符可排除竞争条件。
- 概率基础:我们的概率标志。如果将其设置为0,则不会写入任何请求。如果为100,则表示所有请求都将被记录并包含在统计信息中。
- flush_interval。这是我们将所有数据转储到文件的频率。我们希望在CLI中执行数据收集,但是有些脚本可以长时间执行,如果您使用大量功能,则会占用大量内存。
此外,如果我们的集群负载不那么重,FPM就会知道该工作人员已准备好进行更多处理,并且不会终止该进程-它可以生存并消耗一部分内存。一段时间后,我们将所有内容刷新到磁盘,重置哈希表,然后再次重新填充它。但是,如果尚未达到超时时间,则触发MSHUTDOWN挂钩,最后在此处写入所有内容。
我们想要的最后一件事是能够从PHP代码调用funcmap的功能。相应的扩展名提供了唯一一种使您能够启用或禁用统计信息收集的方法,而不管概率如何起作用。
开销
我们想知道这一切如何影响我们的服务器。我们已经建立了一个图表,显示了向负载最大的PHP集群之一的真实战斗机发出的请求数量。
可以有很多这样的机器,因此该图显示的是请求数,而不是CPU。平衡器意识到机器已开始比平时消耗更多的资源,并尝试均衡需求以使机器平均装载。这足以了解服务器如何降级。
我们依次以25%,50%和100%的价格打开扩展程序,并看到以下图片:
虚线是我们期望的请求数。主线是传入的请求数。我们看到了大约6%,12%和23%的降级:该服务器开始处理的传入请求减少了近四分之一。
该图首先证明了采样对我们很重要:我们不能花费20%的服务器资源来收集统计信息。
错误的结果
采样有一个副作用:统计方法中未包含某些方法,但实际上使用了某些方法。我们试图通过几种方式来解决这个问题:
- . -, . , , , , .
- . , : , , .
我们尝试了两种错误处理解决方案。第一种是从发生错误的那一刻起就强制启用统计信息收集:收集错误日志并进行分析。但是这里有一个陷阱:当资源减少时,错误数量立即增加。您开始处理它们时,会有更多的工作人员-集群开始慢慢消失。因此,这样做并不完全正确。
怎么做不同?我们阅读并使用Nikita Popov的解析器分析了赌注,并指出在那里调用了哪些方法。因此,我们消除了服务器上的负载并减少了误报的数量。
但是仍然有一些方法很少被调用,并且不清楚是否需要它们。我们添加了一个帮助程序,用于确定使用此类方法的事实:如果采样已经显示很少调用该方法,则可以100%打开处理,而不考虑发生了什么。此方法的任何执行将被记录。您会知道的。
如果您确定该方法正在使用中,则可能是过大了。也许这是必需的,但很少见的功能。想象一下您拥有“投诉”选项,该选项很少使用,但很重要-您无法删除它。对于此类情况,我们已经学习了如何手动标记此类方法。
我们创建了一个界面,该界面显示正在使用的方法(背景为白色)和可能不使用的方法(背景为红色)。在这里您还可以标记必要的方法。
界面画面
界面很棒,但是让我们回到最开始,这就是我们正在解决的问题。这是因为我们的工程师读取了无效的代码。他们在哪里阅读?在IDE中。想象一下,要强迫他的技术爱好者通过某种Web界面离开IDE世界并在那里做些什么!我们认为我们需要中途见我们的同事。
我们为PhpStorm创建了一个插件,该插件可加载整个数据库中未使用的方法,并显示是否使用此方法。此外,您可以将方法标记为在界面中使用。所有这些都将发送到服务器,并可供其余的代码库贡献者使用。
到此结束我们与Legacy合作的主要部分。我们开始更快地注意到我们没有在执行,对它的响应更快,并且不再浪费时间手动搜索未使用的代码。
funcmap扩展在GitHub上可用。如果它对某人有用,我们将感到高兴。
备择方案
从外部看来,我们Badoo似乎不知道该如何对待自己。为什么不看看市场上有什么?
这是一个公平的问题。我们看了看-那时市场上没有任何东西。直到我们开始积极实施解决方案时,我们才发现,与此同时,住在雾蒙蒙的英国的乔·沃特金斯(Joe Watkins)实施了类似的想法并创建了Tombs扩展名。
我们没有非常仔细地研究它,因为我们已经有了自己的解决方案,但是尽管如此,我们还是发现了一些问题:
- 缺乏采样。上面,我解释了为什么我们需要它。
- . , APCu ( ), .
- CLI. , , CLI-, .
- . Tombs, , , , , , . funcmap («» , ): , . Tombs , , FPM CLI. - , .
首先,请预先考虑如何删除短时间内实现的功能,尤其是在开发非常活跃的情况下。在我们的案例中,这些是A / B测试。如果您不事先考虑,那么您将不得不清理瓦砾。
第二:通过视线了解您的客户。它们是内部的还是外部的都没有关系-您必须了解它们。在某个时候,您需要告诉他们:“亲爱的,停下!没有”。
第三:清理您的API。这导致整个系统的简化。
第四:您可以自动执行所有操作,甚至可以搜索无效代码。我们做到了。