SegmentFault 滴滴技术最新的文章
2020-08-26T14:38:10+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
可编程网卡芯片在滴滴云网络的应用实践
https://segmentfault.com/a/1190000023773716
2020-08-26T14:38:10+08:00
2020-08-26T14:38:10+08:00
滴滴技术
https://segmentfault.com/u/didijishu
0
<p><img src="/img/bVbLUMV" alt="" title=""></p><p><img src="/img/bVbLUMW" alt="" title=""></p><p><strong>桔妹导读:</strong>随着云规模不断扩大以及业务层面对延迟、带宽的要求越来越高,采用DPDK 加速网络报文处理的方式在横向纵向扩展都出现了局限性。可编程芯片成为业界热点。本文主要讲述了可编程网卡芯片在滴滴云网络中的应用实践,遇到的问题、带来的收益以及开源社区贡献。</p><h2>1. 数据中心面临的问题</h2><p>随着滴滴云规模的不断扩大,业务层面对延迟以及带宽的要求越来越高。2018年滴滴云网络团队上线了基于开源社区的OVS-DPDK方案。DPDK是X86平台报文快速处理的库和驱动的集合, 其主要优势为通过Bypass Linux内核,Hugepage内存以及PMD(Poll Mode Driver)模型驱动的方式实现加速。我们为OVS-DPDK提供了在线热升级功能,该功能保证了在升级过程中虚拟机业务无感知,并且网络Downtime时间为毫秒级别。同时我们优化了OVS-DPDK数据转发平面。实现了不同物理主机上的虚拟机网络延迟<150us,单核性能约~400w pps(双向)。</p><p>滴滴内部上云、高性能计算HPC,以及机器学习,对网络提出了更高的要求。通过CPU DPDK处理报文的方式,虽然在性能以及延迟方面远优于基于Linux 内核的转发实现。但CPU DPDK已经不能满足数据中心流量激增带来的需求。</p><h2>2. 技术方案选择</h2><p>云网络环境中,在计算节点DPDK不会占用过多的CPU,否则会影响CPU售卖,一般会使用1-2 CPU用于数据报文处理。同时DPDK 处理数据报文的性能强依赖CPU算力。因此在计算节点网络的横向扩展以及纵向扩展都具有局限性。</p><p>在边际网关节点,我们可以通过扩展服务器的方式,提高网络处理容量进而满足业务需求。但是大规模的扩展服务器,需要承担更多的机器、功耗以及运维成本。</p><p>软件定义网络(Software Defined Network,SDN)是一种新型网络创新架构,是网络虚拟化的一种实现方式。其核心思想是将网络设备的控制面与数据面分离开来。控制层面可以通过集中控制的方式实现不同的业务逻辑:拓扑发现,路由管理,安全策略,网络虚拟化等。数据平面更专注在数据报文转发。2018年AWS re:Invent,AWS 介绍了Nitro System。该系统通过硬件芯片加速虚拟机IO处理(网络、存储、安全等)。</p><p><img src="/img/bVbLUMX" alt="" title=""></p><p>目前工业界,加速网络处理的焦点聚集到了硬件层面:AISC,FPGA,P4,可编程网卡,以及智能网卡等。</p><h2>3. 基于可编程芯片的解决方案</h2><h3>3.1 硬件芯片选择</h3><ol><li><strong>传统AISC卡</strong></li></ol><p>该卡比较成熟,但业务逻辑固定,很难适应云上复杂的业务场景。</p><ol><li><strong>可编程门队列FPGA</strong></li></ol><p>FPGA 实现网络加速需要专业FPGA技术人员,以及专业网络RD。同时在成本,和研发周期都需要具有一定的局限性。<img src="/img/bVbLUM0" alt="" title=""></p><ol><li><strong>P4</strong></li></ol><p>P4 具有灵活的可编程性,较为合适做为网关节点数据处理。并不适合在计算节点使用。同时价格也是需要考虑的因素。 <br><img src="/img/bVbLUM3" alt="" title=""></p><ol><li><strong>可编程网卡芯片</strong></li></ol><p>通过调研发现,可编程网卡除了具有通用网卡的功能外,还可以通过下发流表规则的方式,实现报文匹配并对报文执行特定的action如:修改,封装,以及转发、上送报文至CPU等。这种具有灵活性、可编程性的硬件芯片,能够满足快速迭代的需求。 </p><p><img src="/img/bVbLUM4" alt="" title=""></p><h3>3.2 转发模型</h3><p>为了满足网元业务灵活性、多样性的需求,我们将网元业务和底层平台功能分离,舍去了传统的数据面Pipeline转发模型,采用了类似Open Flow的macth+action的方式。这样不同的match规则和不同action 匹配能够实现不同的业务逻辑。这种弱依赖的关系能够剥离了业务和底层细节,方便业务功能迭代、快速上线,同时底层可编程芯片的更新不会对业务逻辑产生影响。</p><p><img src="/img/bVbLUM5" alt="" title=""></p><h3>3.3 网络平台化</h3><p>随着云上业务场景的复杂化,以及上云的客户越来越多,云上网络的功能也复杂化。为了统一计算节点以及网关节点功能,我们实现了统一的编程框架。这样能够快速开发不同功能的网关节点,减少运维负担。</p><h3>3.4 落地实践</h3><p>我们基于OVS-DPDK Offload 框架实现流表规则offload。OVS 采用首个报文触发的方式下发硬件流表规则,该方式的优点为在必须的时候下发规则,能够达到节省流表的目的,但是缺点却会导致首个报文延迟。经调研我们发现网卡支持至少百万级流表量(使用x86内存或者其他扩展内存),最终我们舍去OVS-DPDK ofproto 转发层,使用dpctl 接口下发流表,这样就不存在首个报文延迟问题,同时也缩减了使用TC Flower时数据面过多问题(这些转发平面包括:硬件芯片转发,TC数据面,OVS Linux 内核模块转发,以及ofproto层)。我们修改了OVS-DPDK 流表老化方式,保证通过dpctl 下发的规则不会被删除。最后通过upcall limit 限制了upcall 报文处理。滴滴云网络数据平面主要分为两大部分:计算节点和网关节点。计算节点主要负责虚拟机、容器网络的虚拟化,网关节点主要负责各种边际节点业务如:SLB负载均衡、vRouter EIP报文处理,分流器、SNAT、FullNAT、云企业网等。可编程网卡芯片通过平台化的方式在两个主要节点均有应用。</p><p><img src="/img/bVbLUM6" alt="" title=""></p><ol><li><strong>SLB负载均衡</strong></li></ol><p>提供四层负载均衡,根据用户策略将underlayer网络报文分发到虚拟网络服务节点。</p><ol><li><strong>vRouter</strong></li></ol><p>提供弹性EIP服务。用户可以将一个公网IP地址绑定到虚拟机、容器、或者裸金属,从而获得公网访问功能。</p><ol><li><strong>iRouter</strong></li></ol><p>将滴滴数据中心和滴滴云虚拟网络打通,滴滴数据中心可以方便快捷的访问云上资源。</p><ol><li><strong>SNAT</strong></li></ol><p>为虚拟机、容器以及裸金属提供访问公网服务。</p><ol><li><strong>云企业网互联</strong></li></ol><p>互联服务支持将滴滴云上的多个VPC网络加入云互联,任意两个VPC网络即可实现资源之间的互访。</p><ol><li><strong>计算节点</strong></li></ol><p>在计算节点主要有两大应用场景:一种场景为在计算节点为虚拟机、容器提供VPC服务(网络隧道,限速,转发,报文修改,公网服务),RDMA网络。另外一个场景使用智能网卡为裸金属提供VPC服务。</p><h3>3.5 遇到的问题</h3><p>在调研开发过程中遇到诸多问题,在这里和大家总结分享下:</p><ol><li><strong>OVS-DPDK 支持Offload 程度有限</strong></li></ol><p>首先OVS 社区并对DPDK Offload接口(rte flow)支持有限:实现的action非常有限。需要使用者独立完成开发:如set action,meter offload,vxlan 隧道报文处理等。</p><ol><li><strong>端口转发限制</strong></li></ol><p>目前mellanox网卡芯片并不支持从一个PF端口转发到该芯片另一个端口, 最终我们通过SRIOV+Hairpin的方式解决该问题。据了解后续的网卡芯片开始支持该功能(功能也受限于固件)。</p><ol><li><strong>Open vSwitch Crash</strong></li></ol><p>在删除包含meter action 流表规则时,OVS 进程退出。该问题最终确认为DPDK的一个bug,目前该问题已经修复,发送到社区并接收。<a href="https://link.segmentfault.com/?enc=qcvNykvgrOYIrCXnrW%2BYTg%3D%3D.%2B4trZeuTpfvbuOCFf10zrF1xB%2BLg%2BnqzwhdQpWZlXIWqMuJ2z0LIyTZBQ2%2BEyCmSLGZOUxSXaseTkH9yiZ%2Fm7xPCsVO%2FbwXXz7nkeYEJt4YRQVmZrWQdD6RBvD%2Fu09cN" rel="nofollow">http://git.dpdk.org/next/dpdk...</a></p><p>调用DPDK Meter API 接口导致crash。目前该问题已经修复,发送到社区并接收。<br><a href="https://link.segmentfault.com/?enc=g3j9%2FKCR1Nv9dMPnF7%2Bx%2Fg%3D%3D.8U9PPCSV0o9qJDB9taWrUyh3aqPEP5tZ5Iy%2F9c%2BikREtmqJ3AUzHehyoTjJQLKaeVMZ7b5JHAELiQ9uf%2B9cDnh5vcTMf5oVj11qq9oCiD0ZTeLzAZZIvwpkk8bBx9tdN" rel="nofollow">http://git.dpdk.org/next/dpdk...</a></p><p>修改OVS 配置导致删除offload flow crash,目前该问题已经修复,发送到社区并接收。<a href="https://link.segmentfault.com/?enc=E02JfZzi3AlcFW0GkDDY9Q%3D%3D.YL8TVjl%2Bm3YopiGYZfZqTNbGxRjyVawLGnagksRcXcg1a41QQQ%2Bloy0wj2WAGfStDUIYjyjMwc0TndLZiSEyssfLGZJt5lbui2nizVSZKcgaoe1MdBR2meuOpV87g8w2" rel="nofollow">https://github.com/openvswitc...</a></p><ol><li><strong>Meter offload</strong></li></ol><p>OVS社区没有实现该功能,我们根据业务特征抽象出接口并在OVS实现了meter offload。该系 列补丁文件正在OVS 社区review,不久会进入upstream。</p><ol><li><strong>Decap/Encap 流表限制</strong></li></ol><p>下发多条带有decap/encap的流表规则时报错。该问题最终确认为DPDK的一个bug,目前该问题已经修复,与社区maintainer 协同修复。<a href="https://link.segmentfault.com/?enc=cMf6PyZNfIOYCa5q0Ii3aA%3D%3D.3asf0xNp3GPtT4QL1%2B%2FN%2FY7%2BZRMVCQJIvv5BcBkXkMhKq40%2FO%2BqM2L0mfkg2qgAbZP54VNz4%2Fqf2rlJiB9aXoKMN3BHRsoIWrj%2BbW%2BU%2BC6OFS5OP5ZwHvxdtRQlE3BvX" rel="nofollow">http://git.dpdk.org/next/dpdk...</a></p><ol><li><strong>Decap + Meter action限制</strong></li></ol><p>decap + meter 做为action 下发规则时失败。该问题最终确认为DPDK的一个bug,目前该问题已经修复,与社区maintainer 协同修复<a href="https://link.segmentfault.com/?enc=j50a1CGpWYxa2MWfZ76%2BAQ%3D%3D.VHQ3En3QS4bDWDboMPKIvcAo3EpIl1W5DX5c%2BXadudOXKvMrxFymOvj0AZATBbgburTAaQd8lX0Syg2kJ4%2BfP44yXcweRliBIOTx5zVf4agIXwndmOohgRcks1U5ZZA4" rel="nofollow">http://git.dpdk.org/next/dpdk...</a></p><ol><li><strong>Hairpin 性能问题</strong></li></ol><p>在高并发情况下,mellanox 网卡芯片性能会下降约40%,最终确认是网卡驱动hairpin问题。目前mellanox 确认该问题并给出修复方式。</p><ol><li><strong>流表数目限制</strong></li></ol><p>通过删除流表上限修复该问题:<a href="https://link.segmentfault.com/?enc=EmGHNUu1X55N9sydBtycvg%3D%3D.ic3FKxvZCOwTeYrotQsflwBqiRhNChrgQedJ36U%2B8j7uHi210wLuemqVp55cam7jp%2Fb%2BJpSlpthhqhZxf9cn2bEUWIotrzwXfD0FWGzPwYC8n%2FYhiUmFA0N51vvaF9px" rel="nofollow">https://github.com/openvswitc...</a></p><ol><li><strong>MAC 地址对VxLAN的影响</strong></li></ol><p>物理主机源MAC地址变更后vxlan 报文依旧使用原来MAC地址,这样会导致收不到响应报文:<br><a href="https://link.segmentfault.com/?enc=GbweJ%2F%2BD8b3kWXrTVSpQhQ%3D%3D.eGuNlwZOROlmRKLQKL1Bhfb3Kxs6DkDQ4gLjFJzUO2pFsw4qHHNeFNM6h3o1E3nzWPFukd40cK2ed0sPS7rmlQ%2Br%2Bu51JcSXyVLSBw98WlQVdw6U1PNoacYG7WfGr9RkYuiP3%2F7NVIa%2FwujmR7kyh7uZIWNo8Tqn9dvi%2FMoxiCI%3D" rel="nofollow">https://git.kernel.org/pub/sc...</a></p><ol><li><strong>多次修改报文不生效问题</strong></li></ol><p>多次使用TC Flower pedit 修改报文,offload 不生效问题, 最终确认是内核驱动问题:<br><a href="https://link.segmentfault.com/?enc=fW7JFISiG5OLZOeRbzkYTA%3D%3D.VERcaLQgqO%2Bac%2BvmEBJD7ocrWLj7F1qMdgHsygKc8ScTwWz6%2B%2BF0CgDRbjmIxFCjkgpugFFBPKrAFm%2FW4QmdbxbHnSpBstwo5n%2F1Jw5WV1eNDKkAqhSwoGn3MFVoKCW4iSupjU2y9xFljO8SSPErKZ0nZQyZlWhHkTGeFQi0sHA%3D" rel="nofollow">https://git.kernel.org/pub/sc...</a></p><ol><li><strong>配置vf rate、mac不当导致内核crash</strong></li></ol><p><a href="https://link.segmentfault.com/?enc=xdRV%2BuYdi4QlgTktOo27vA%3D%3D.NFhFrf0a6N6OIbZ%2FH4DhIQ3ACiUALEa%2F%2FSjweQcBXlynL9J7Oo3TytspFIjcENdX5mTxu%2BZ0a11b%2BByJOqDcH1y6lZs8oN%2FYFARc2h2PDI7yV9S1OdGPxSTGsTwGld0PO8MNHhqH01eqIG5qQZnVfNNMPEz6ONhVYV7vOocIrVc%3D" rel="nofollow">https://git.kernel.org/pub/sc...</a></p><p><a href="https://link.segmentfault.com/?enc=j9f65ohwIJ3gU%2B7nReuAUg%3D%3D.5kAJ%2B%2BOkybloos53dPCDu6RQl04hKg9TpaRFW%2FeeulCry57l0NzFaAVFvqH7pm7y1EXJbMXDPQSh3%2Fw4pYP%2Fz3NyO5FethItYBuG1x2opow%3D" rel="nofollow">https://git.kernel.org/pub/sc...</a> id=24319258660a84dd77f4be026a55b10a12524919</p><h3>3.6 性能数据</h3><p>以实现的vRouter 弹性公网网关为例(基础网络10Gpbs):</p><table><thead><tr><th>pps(64B)</th><th align="center">Mpbs(64B)</th><th align="center">pps(1500B)</th><th align="center">Mpbs(1500B)</th></tr></thead><tbody><tr><td>9495892</td><td align="center">8660.25</td><td align="center">811935</td><td align="center">10067.98</td></tr></tbody></table><p>业务延迟数据如下(使用pktgen-dpdk latency):</p><table><thead><tr><th>背景流量</th><th>网关延迟</th></tr></thead><tbody><tr><td>10W条流表以及并发1Gbps 64B流量</td><td>3u</td></tr><tr><td>10W条流表以及并发5Gbps 64B流量</td><td>6u</td></tr></tbody></table><h2>4. 开源社区贡献</h2><p>除了为开源社区提供 bug patch,我们也将新增特性、性能优化patch回馈至开源社区:OVS、DPDK、Linux 内核社区(约80+ patch), 其中Linux 内核补丁列表如下:</p><p><img src="/img/bVbLUM7" alt="" title=""></p><p><img src="/img/bVbLUug" alt="" title=""></p><p><strong>团队介绍</strong></p><p>滴滴云平台事业群滴滴SDN网络团队负责云网络产品的规划、设计、以及研发等工作。为公有云提供负载均衡SLB、专有网络VPC、弹性公网EIP、SNAT 以及云互联等服务。团队针对云网络业务需求,在Linux 内核网络虚拟化、DPDK、OVS、可编程芯片、RDMA、智能网卡以及系统优化等领域均有广泛深入的研究。团队具有多名开源社区contributor,涉及OVS、DPDK、Linux 内核等。</p><p><strong>作者介绍</strong><br><img src="/img/bVbLUM8" alt="" title=""></p><p>专注于高性能网络技术,从事云网络研发工作。活跃于Linux 内核、OVS、DPDK开源社区。</p><p><strong>延伸阅读</strong></p><p><a href="https://link.segmentfault.com/?enc=fbdwNMRojudiB8P3RVOwbw%3D%3D.K6eH4R6Y2rO%2F9tlsDsy2vhTl0h4vu%2FBlYSxckV0FGXg3u9V5sUWAUIcs53C12j%2BOE730cxp0r2CopnReOfLtoZwOjZLlKvTHW4ZOUiwg0bSKJxDsWz%2BnAktG%2BmCeBWkUcmTWm%2FX0jZoI7%2FGdCCApEUiPikPbIuOPOnW7jGH%2Fq8eGDOVUHXSX7ldCrEbcREU5paZsr09oCP1YGF9MgjgYpIwJiydVbuNLi%2ByYqPbZvL%2FUsstn3kLg9JMZYkQMklmsNfo8OyqDozBuOZQG%2FPGVDUKUssKo6P3ghbpjub0xsd%2Fysoau%2Bv6qtJF8cYk05PDZ" rel="nofollow"><img src="/img/bVbLUM9" alt="" title=""></a> </p><p><a href="https://link.segmentfault.com/?enc=MsWLa8ss8TPiZpGSCJv34Q%3D%3D.gJ5wai3udKJsAJ1Qlj5hUWFn8qiwGeUc1P7ULbHc0xVp%2B0mf9mAdz1ZSI9TGUx1zljunPgBNYRQKfQyKUZOYI4ahjtXaPJJ0OVOQfVePCcALm%2Fn%2BOZCaqva9UdooMjYH4Qja5GY1cBhnvfDer5gRG%2FrRE3eqh%2FDtkBJ8%2BiowrVrBWWeB26wcCGMcAglEBiSej6TD5dYlWOjVGWE2lCMNkjha6PVXnQroB4xyZjudihOIV837w3DpO%2Fxg1%2Fd48yBSz2%2FVUV%2BLg%2FWV%2BEs%2B%2BZcs4V3MGM7AmwWXAnZMXVRlc9n6Zf984ckzA6%2F99hyMnPX8" rel="nofollow"><img src="/img/bVbLUNa" alt="" title=""></a> </p><p><a href="https://link.segmentfault.com/?enc=cV7GWD9Gr3AUOgMs%2Bc25qg%3D%3D.U1F7s34xNJtiDhakuLQpW3hb5kvPnlAND2eHr6%2BB2OFDQg0n2zmLPJxgxBkYubUh9N3ZI%2Ff6JFLLJnNCFP134%2BgkdqetcZixobJKSrMtSrmnTIup3XkS9lJjqk4hoakuF24Pd9HfOVctIb3gYCw778zXefd929Hd93FMM8I4oEvCzcHPCmHbYg3NIOAcM9mLuYPINDiYXAa9bCrM2sNbKVmz5MW%2F12O5nqjax%2B4X0vi4RtBpDubuTrEb4uDufqMUMrLb1O6NXlOgmRCWOU5sT3j%2FVOU182Svifawdh93hhFBOfntE9rE01DG2n1iyMoP" rel="nofollow"><img src="/img/bVbLUui" alt="" title=""></a> </p><p>内容编辑 | Charlotte & Teeo<br>联系我们 | DiDiTech@didiglobal.com</p><p><img src="/img/bVbLUNb" alt="" title=""></p><blockquote>滴滴技术 出品</blockquote>
滴滴大数据在汽车金融风控场景中的应用
https://segmentfault.com/a/1190000020120926
2019-08-19T20:48:37+08:00
2019-08-19T20:48:37+08:00
滴滴技术
https://segmentfault.com/u/didijishu
3
<h2>导读:</h2>
<p>滴滴独有的出行场景大数据在金融领域有着非常广泛的应用前景,未来可与银行,保险,支付和理财等机构深入合作,帮助传统金融机构提升资源配置效率,降低获客和风险管理成本。出行场景大数据在交易欺诈识别、风险定价、精准营销、全生命周期风险管理、增长运营等方面都有着重要商业价值。对于大数据的应用分析能力,正在成为金融机构未来发展的核心竞争要素。本文从汽车金融车贷产品的视角切入,将场景数据与传统信贷风控理念相结合,准确识别业务开展过程中的信用风险变化,对完善业务模式和重塑用户价值起到了积极的作用。</p>
<h2>0.目录</h2>
<ol>
<li>汽车金融是什么?</li>
<li>滴滴汽车金融在做什么?</li>
<li>
<p>滴滴大数据在汽车金融风控上的应用</p>
<ul>
<li>从资产端视角看存在问题和解决方案</li>
<li>从全流程风险管理视角看存在问题和解决方案</li>
<li>数据应用上的三个优化点</li>
</ul>
</li>
<li>
<p>滴滴大数据在汽车金融风控场景下的应用前景</p>
<ul>
<li>企业信贷智能风控</li>
<li>零售信贷智能风控</li>
</ul>
</li>
</ol>
<h2>1.汽车金融是什么?</h2>
<p>汽车金融主要指与汽车产业相关的金融服务,是在汽车研发设计、生产、流通、消费等各个环节中所涉及到的资金融通方式。主要包括资金筹集、信贷分期、抵押贴现、金融租赁,以及相关保险、投资等活动。</p>
<h3>▍商业模式</h3>
<p>零售业务中,商业银行和融资租赁公司作为资金方,经销商/4S店/租赁公司作为销售渠道,汽车电商平台起到导流作用,共同为有购车需求的个人消费者提供分期购车金融产品和服务。</p>
<p><img src="/img/remote/1460000020120929" alt="" title=""></p>
<p>从竞争格局看,银行和厂商金融是零售市场的主要玩家,在资金成本和渠道获客上占有绝对优势。此外,汽车电商平台作为线上导流服务方,为传统金融机构提升获客效率,近几年也活跃在汽车金融市场。从产品类型上来看,售后回租为市场主流,直租有待快速发展。</p>
<p><img src="/img/remote/1460000020120930?w=1372&h=570" alt="" title=""></p>
<h2>2.滴滴汽车金融在做什么?</h2>
<p>1)滴滴汽车金融业务现阶段定位为服务出行生态,一切从用户价值出发,为有购车需求的司机提供低成本购车金融方案。</p>
<p>2)对内构建汽车金融风控体系,通过网约车场景数据的积累和应用,不断提升全面风险管理能力,生成优质网约车金融资产,逐步形成风险定价能力。</p>
<p>3)对外向传统金融机构提供优质金融资产和系统化的风控能力输出,实现资金和资产高效匹配,积累金融资产管理能力。与此同时,作为连接资金和资产的双边平台,与主流金融机构建立长期合作伙伴关系,持续为网约车体系提供资金支持。</p>
<p>未来滴滴汽车金融的业务范围会随着出行产业生态的发展不断丰富, 延伸至整个出行产业链,为汽车经销商、4S店、代理商等汽车销售者采购汽车和营运设备提供的金融服务, 以满足产业链上下游各环节的金融需求,逐步形成集信息流、资金流、物流于一体的汽车产业金融新业态。</p>
<h2>3.滴滴大数据在汽车金融风控上的应用</h2>
<p>传统信贷框架下,以贷款人央行征信判定还款能力的风控模式已经不再满足网约车金融的风险管理需求。网约车场景下,汽车金融风控对在贷资产的真实性、稳定性、以及风险预警的时效性提出了更高要求,基于大数据建立智能营销和智能风控决策体系显得尤为重要。</p>
<h3>▍从资产端来看:</h3>
<p>车贷C端问题: 贷前准入未使用场景内数据作为个人征信补充,贷中数据缺失,没有匹配的风险预警方案,贷后催收效率低,需要对网约车贷款人形成动态信用评分。</p>
<p>解决方案:运用滴滴大数据补充传统零售评分卡模型,将场景中能够反映个人信用风险特征的数据应用到汽车金融领域,制定风控政策和准入标准。同时建立体系内有车群体的PD(probability of default)评分模型,关注PD参数的显著变化,提供大数据下的风险预警方案。逐步搭建网约车场景下的全面风险管理体系,提升全流程风险管理能力。</p>
<p>车贷B端问题:传统金融机构对于CP(Car partners)征信数据的缺失,导致其不能有效识别渠道风险,尤其对于中小型CP来说,很难获得传统金融机构的授信。</p>
<p>解决方案:借助滴滴平台大数据,支持资方对CP的授信审批。具体来说,是将渠道基础信息,以及能够反映其资产规模,资产使用效率,司机管理能力的数据维度进行系统化梳理,形成入模变量,同时不断积累体系内坏样本,建立CP半监督模型。模型输出结果即是CP信用评级综合分数,直观反映出CP的风险等级。目前汽车金融的CP评级为月度输出,可以动态反映出CP风险等级的变化。</p>
<h3>▍从全流程风险管理来看:</h3>
<p>在实际运营过程中,我们在零售车分期贷款的贷前,贷中和贷后三个阶段发现了以下问题。</p>
<p>贷前准入风险:贷款申请人不是放款后实际运营该车辆的司机,也就是说A贷B还。这种问题通常发生在渠道进件环节。汽车金融产品销售过程中存在一定的操作风险,线下渠道销售人员为了提高成单率,找了信贷资质好,更容易通过贷前审核的人代替司机申请贷款,然而实际跑滴滴的司机信贷资产差,还款能力不足以支持月供,PD违约概率较高。那么这笔车分期贷款的信用风险就会在贷后的资产表现期内逐渐释放。</p>
<h3>▍首次拉单时,贷款人和司机信息不符:</h3>
<p><img src="/img/remote/1460000020120931" alt="" title=""></p>
<p>贷中运营风险:贷款人在存续期内退车,车辆由租赁公司代偿,待租赁公司找到新司机后由新司机运营并继续还款。这种情况下, 传统风控在贷前准入对初始贷款人的判断,以及车辆GPS定位已经不再能够有效反映贷后运营车辆的风险变化。在贷车辆在存续期内先后匹配多个滴滴司机时,租赁公司在车辆运营管理,现金流管理和司机管理上面临很大挑战,有时多个司机集中退车会引起渠道集中性风险。</p>
<h3>▍运营中一辆车在不同时点匹配多个司机:</h3>
<p><img src="/img/remote/1460000020120932?w=1568&h=788" alt="" title=""></p>
<p>贷后逾期催收:传统信贷风控对于网约车贷后数据缺失,在无法获得贷款人收入以及营运行为数据的情况下,不能确定每笔逾期债项背后贷款人的还款能力和还款意愿,因而无法做到对收入还贷比高,有还款能力的贷款人进行优先催收。这种情况下,需要针对贷款人平台拉单数据以及贷款车辆营运数据制定催收评分卡,对催收进行分类管理。</p>
<h3>▍滴滴大数据可以解决:</h3>
<h4>网约车金融全面风险管理体系的搭建。</h4>
<p>在零售数据准备和模型变量开发时,形成从贷款人信贷基础维度到涵盖城市、渠道、车辆四大风险因子的模型长清单,实现覆盖在贷资产全生命周期的动态监控。同时通过被投企业资产表现不断积累模型因变量(坏样本),有效把握风险等级变化,建立预警和响应机制,降低损失率。</p>
<p><img src="/img/remote/1460000020120933?w=576&h=286" alt="" title=""></p>
<p>每个风险因子下钻形成多个风险指标,组合后形成风控策略。通过单一策略和多策略的综合应用,实现贷中预警和风险的及时防范。</p>
<p>具体来说,优化方向有以下几点:</p>
<p><strong>优化点1:从传统的放款时点贷款人风险评估,优化为全流程多维风险动态监测。</strong></p>
<p>传统信贷风控只注重贷款人单一维度的信用风险计量,而在网约车场景下,城市政策合规、车辆运营状态、渠道管理能力都会在整个信贷流程中对信用风险的变化起到决定性作用。对此我们借助滴滴网约车场景数据和坏样本的持续积累,来补充传统信贷数据维度, 优化A卡和B卡。</p>
<p>预警需求分析:</p>
<p><img src="/img/remote/1460000020120934" alt="" title=""></p>
<p><strong>放款时点:</strong><br>反欺诈信息核实,数据维度包括但不限于平台侧核实司机、车辆、人车匹配、渠道基础信息,同时排查渠道进件风险。</p>
<p>放款后,通过贷中监控实时反映贷款人信用风险变化,建立大数据风险预警体系。</p>
<p>建立大数据内评验证治理架构,内评验证流程方法,提供不同层次的的优化策略和实时流程。预警模型中,典型贷中预警策略如下:</p>
<p>司机维度策略:流水稳定性,收入能力,是否已办理人证等。<br>车辆维度策略:车辆在平台运营情况,车辆和司机的匹配情况,车辆行驶里程,是否已办理车证等。<br>CP渠道策略:渠道负面信息扫描,渠道集中性风险事件,合规比例,渠道集中性逾期等<br>城市合规策略:是否已获取网约车平台证,城市合规人证办理进度,是否分类管理等。</p>
<p>随着数据维度不断丰富,四大风险因子的下钻维度会逐步增多。我们同时也在实际业务中逐一验证,并通过司机A卡B卡模型结果进行策略迭代。</p>
<p><strong>贷后催收:</strong><br>优化催收评分模型。实时对逾期司机的逾期天数,拉单行为,月均收入进行分析和监控,得出每笔逾期债项对应的还款能力和还款意愿综合评分列表,帮助贷后催收提升效率。</p>
<p><strong>优化点2:增加数据观测的时间宽度和时点观测深度,并在此基础上引入前瞻性。</strong></p>
<p>通过对数据的长期观测,单一风险策略迭代以及多策略应用的持续验证,我们会得到司机信用风险变化的历史平均水平和规律,结合业务现阶段和未来发展趋势,在此基础上得到前瞻性调整后的PD(违约概率),对信用风险的显著变化进行定量和定性评估。</p>
<p><strong>优化点3:依托大数据分析能力,形成对业务全局风险收益变化的综合判断。</strong></p>
<p>通过C端融租车辆的全流程风险管理,逐步勾勒出了融租产品形态下的司机信贷画像和CP渠道画像, 快速识别汽车金融在业务模式和产品上的运营风险,比如融租包经租,CP代偿,集中性违约风险等。进而对车金融资产质量有清晰准确的计量,实现资产端和资金端风险收益的平衡。</p>
<p><img src="/img/remote/1460000020120935" alt="" title=""></p>
<h2>4.滴滴大数据在汽车金融场景下的广泛应用前景</h2>
<h3>▍企业信贷智能风控</h3>
<p>方向:整个出行行业生态中,存在大量分散的中小企业服务商/渠道商,这些中小企业在滴滴平台上的日常经营数据反映了其经营能力、资金流动性管理和司机管理能力。多维度经营数据完全可以支持数据风控方式获得资金,为业务提供决策创新方案,包括识别客户异常行为、差异化授信审批、全流程风险管控和预警、限额设定等。</p>
<p>进展:目前一些与滴滴平台合作方有业务往来的汽车金融持牌机构已经在与我们就数据风控的授信方式进行深入探讨,在平台不提供担保的情况下,通过司机余额代扣和平台多维度数据建立风控模型,为优质汽车租赁公司提供对公授信资金支持。</p>
<h3>▍零售信贷智能风控</h3>
<p>滴滴平台具有明显的双边效应,即供给侧和需求侧都通过平台完成交易,因此平台上会沉淀大量交易和运营数据。当汽车金融服务对象是体系内有车人群时,可通过滴滴大数据补充传统零售评分卡的不足, 将体系内非信贷数据应用到汽车金融业务场景下,比如用于制定产品级的风控政策和准入标准,输出自动化信用评分,反欺诈,风险敞口管理, 风险定价等。</p>
<p>逐步建立网约车场景下的风险管理体系, 实现内评模型在数据、决策、和算法层面的创新。</p>
<p><img src="/img/remote/1460000020120936" alt="" title=""></p>
<p>包括:前筛客群、特征模型建立和训练、反欺诈规则设计、线上策略验证、与合作伙伴联合建模、线上贷后逾期管理等。</p>
<p>随着大数据风控能力积累,不管产品形态是新车融资租赁还是车辆抵押贷款,都可以针对不同业务类型,建立智能风控体系。在此基础上,平台数据的动态监控能够帮助筛选资产表现良好的个人信贷用户,形成白名单,自动化审批放款,提升资产匹配效率。</p>
<h2>本文作者:</h2>
<p>唐佩<br>滴滴 | 汽车金融商业分析师</p>
<p>一个有着金融业管理咨询背景的工科生,认为人生的意义和有价值的工作强相关,一直都在寻找聪明机智,有深度思考习惯,对商业高度敏感,视野广阔的合作伙伴加入队伍。 </p>
<p>同时,也欢迎您关注滴滴技术公众号,我们会为您带来最新的开源信息和技术干货!</p>
<h2>滴滴技术公众号:</h2>
<p><img src="/img/remote/1460000020120937" alt="" title=""></p>
AoE:一种快速集成AI的终端运行环境SDK
https://segmentfault.com/a/1190000020071335
2019-08-14T16:23:31+08:00
2019-08-14T16:23:31+08:00
滴滴技术
https://segmentfault.com/u/didijishu
6
<h2>一、背景</h2>
<h3>1.1 AoE是什么</h3>
<p>AoE (AI on Edge) 是一个滴滴开源的终端侧AI集成运行时环境(IRE)。以 “稳定性、易用性、安全性” 为设计原则,帮助开发者将不同框架的深度学习算法轻松部署到终端高效执行,Github 地址是 <a href="https://link.segmentfault.com/?enc=lU9uie1z%2FSd3UJrjbgy4rQ%3D%3D.yCM8V%2B5LZTlIqwOIrrErjsNEjexsEk33lJiHAUhHcaI%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=ejSqHUsaV8sKsNCBkuPopw%3D%3D.jBjK5j2cE81d85%2BWApzc90cFePa%2BZFu40zrCmP2O19g%3D" rel="nofollow">https://github.com/didi/aoe</a>。</p>
<p><strong>为什么要做一个 AI 终端集成运行时框架,原因有两个:</strong></p>
<p><strong>一是随着人工智能技术快速发展,这两年涌现出了许多运行在终端的推理框架,在给开发者带来更多选择的同时,也增加了将AI布署到终端的成本;</strong></p>
<p><strong>二是通过推理框架直接接入AI的流程比较繁琐,涉及到动态库接入、资源加载、前处理、后处理、资源释放、模型升级,以及如何保障稳定性等问题。</strong></p>
<p>目前AoE SDK已经在滴滴银行卡OCR上应用使用,想更加清晰地理解 AoE 和推理框架、宿主 App 的关系,可以通过下面的业务集成示意图来了解它。</p>
<p><img src="/img/bVbwnBZ?w=551&h=376" alt="图片描述" title="图片描述"></p>
<h3>1.2 终端推理框架一览</h3>
<p>下面是终端运行的8种主流推理框架(排名不分先后)。</p>
<p><img src="/img/bVbwnyX?w=621&h=778" alt="图片描述" title="图片描述"></p>
<h3>1.3 AoE 如何支持各种推理框架</h3>
<p><strong>从本质上来说,无论是什么推理框架,都必然包含下面 5 个处理过程,对这些推理过程进行抽象,是 AoE 支持各种推理框架的基础。</strong></p>
<p>目前,AoE 实现了两种推理框架 NCNN 和 TensorFlow Lite 的支持,以这两种推理框架为例,说明一下 5 个推理过程在各自推理框架里的形式。</p>
<p><img src="/img/bVbwny6?w=1055&h=216" alt="图片描述" title="图片描述"></p>
<h3>1.4 AoE 支持哪些平台</h3>
<p>目前,AoE 已经开源的运行时环境 SDK 包括 Android 和 iOS 平台,此外 Linux 平台运行时环境 SDK 正在紧锣密鼓地开发中,预计在9月底也会和大家正式见面。</p>
<h2>二、工作原理</h2>
<h3>2.1 抽象推理框架的处理过程</h3>
<p>前面已经介绍了,不同推理框架包含着共性的过程,它们分别是初使化、前处理、执行推理、后处理、释放资源。对 AoE 集成运行环境来说,最基本的便是抽象推理操作,通过 依赖倒置 的设计,使得业务只依赖AoE的上层抽象,而不用关心具体推理框架的接入实现。<strong>这种设计带来的最大的好处是开发者随时可以添加新的推理框架,而不用修改框架实现,做到了业务开发和 AoE SDK 开发完全解耦</strong>。</p>
<p>在 AoE SDK 中这一个抽象是 InterpreterComponent(用来处理模型的初使化、执行推理和释放资源)和 Convertor(用来处理模型输入的前处理和模型输出的后处理),InterpreterComponent 具体实现如下:</p>
<pre><code>/**
* 模型翻译组件
*/
interface InterpreterComponent<TInput, TOutput> extends Component {
/**
* 初始化,推理框架加载模型资源
*
* @param context 上下文,用与服务绑定
* @param modelOptions 模型配置列表
* @return 推理框架加载
*/
boolean init(@NonNull Context context, @NonNull List<AoeModelOption> modelOptions);
/**
* 执行推理操作
*
* @param input 业务输入数据
* @return 业务输出数据
*/
@Nullable
TOutput run(@NonNull TInput input);
/**
* 释放资源
*/
void release();
/**
* 模型是否正确加载完成
*
* @return true,模型正确加载
*/
boolean isReady();
}
</code></pre>
<p>Convertor的具体实现如下:</p>
<pre><code>interface Convertor<TInput, TOutput, TModelInput, TModelOutput> {
/**
* 数据预处理,将输入数据转换成模型输入数据
*
* @param input 业务输入数据
* @return 模型输入数据
*/
@Nullable
TModelInput preProcess(@NonNull TInput input);
/**
* 数据后处理,将模型输出数据转换成业务输出数据
*
* @param modelOutput 模型输出数据
* @return
*/
@Nullable
TOutput postProcess(@Nullable TModelOutput modelOutput);
}
</code></pre>
<h3>2.2 稳定性保障</h3>
<p>众所周知,Android平台开发的一个重要的问题是机型适配,尤其是包含大量Native操作的场景,机型适配的问题尤其重要,一旦应用在某款机型上面崩溃,造成的体验损害是巨大的。有数据表明,因为性能问题,移动App每天流失的活跃用户占比5%,这些流失的用户,6 成的用户选择了沉默,不再使用应用,3 成用户改投竞品,剩下的用户会直接卸载应用。因此,对于一个用户群庞大的移动应用来说,保证任何时候App主流程的可用性是一件最基本、最重要的事。结合 AI 推理过程来看,不可避免地,会有大量的操作发生在 Native 过程中,不仅仅是推理操作,还有一些前处理和资源回收的操作也比较容易出现兼容问题。<strong>为此,AoE 运行时环境 SDK 为 Android 平台上开发了独立进程的机制,让 Native 操作运行在独立进程中,同时保证了推理的稳定性(偶然性的崩溃不会影响后续的推理操作)和主进程的稳定性(主进程任何时候不会崩溃)。</strong></p>
<p>具体实现过程主要有三个部分:注册独立进程、异常重新绑定进程以及跨进程通信优化。</p>
<p>第一个部分,注册独立进程,在 Manifest 中增加一个 RemoteService 组件,代码如下:</p>
<pre><code><application>
<service
android:name=".AoeProcessService"
android:exported="false"
android:process=":aoeProcessor" />
</application>
</code></pre>
<p>第二个部分,异常重新绑定独立进程,在推理时,如果发现 RemoteService 终止了,执行 “bindService()” 方法,重新启动 RemoteService。</p>
<pre><code>@Override
public Object run(@NonNull Object input) {
if (isServiceRunning()) {
...(代码省略)//执行推理
} else {
bindService();//重启独立进程
}
return null;
}
</code></pre>
<p>第三个部分,跨进程通信优化,因为独立进程,必然涉及到跨进程通信,在跨进程通信里最大的问题是耗时损失,这里,有两个因素造成了耗时损失:</p>
<p>传输耗时<br>序列化/反序列化耗时</p>
<p><strong>相比较使用binder机制的传输耗时,序列化/反序列化占了整个通信耗时的90%。由此可见,对序列化/反序列化的优化是跨进程通信优化的重点。</strong><br><strong>对比了当下主流的序列化/反序列化工具,最终AoE集成运行环境使用了kryo库进行序列化/反序列。</strong>以下是对比结果,数据参考oschina的文章《各种 Java 的序列化库的性能比较测试结果》。</p>
<h2>三、MNIST集成示例</h2>
<h3>3.1 对TensorFlowLiteInterpreter的继承</h3>
<p>当我们要接入一个新的模型时,首先要确定的是这个模型运行在哪一个推理框架上,然后继承这个推理框架的InterpreterComponent实现,完成具体的业务流程。MNIST是运行在TF Lite框架上的模型,因此,<strong>我们实现AoE的TF Lite的Interpreter抽象类,将输入数据转成模型的输入,再从模型的输出读取业务需要的数据。</strong>初使化、推理执行和资源回收沿用TensorFlowLiteInterpreter的默认实现。</p>
<pre><code>public class MnistInterpreter extends TensorFlowLiteInterpreter<float[], Integer, float[], float[][]> {
@Nullable
@Override
public float[] preProcess(@NonNull float[] input) {
return input;
}
@Nullable
@Override
public Integer postProcess(@Nullable float[][] modelOutput) {
if (modelOutput != null && modelOutput.length == 1) {
for (int i = 0; i < modelOutput[0].length; i++) {
if (Float.compare(modelOutput[0][i], 1f) == 0) {
return i;
}
}
}
return null;
}
}
</code></pre>
<h3>3.2 运行时环境配置</h3>
<p>接入MNIST的第二个步骤是配置推理框架类型和模型相关参数,代码如下:</p>
<pre><code>mClient = new AoeClient(requireContext(), "mnist",
new AoeClient.Options()
.setInterpreter(MnistInterpreter.class)/*
.useRemoteService(false)*/,
"mnist");
</code></pre>
<h3>3.3 推理执行</h3>
<p>以下是MINST初使化推理框架、推理执行和资源回收的实现:</p>
<pre><code>//初使化推理框架
int resultCode = mClient.init();
//推理执行
Object result = mClient.process(mSketchModel.getPixelData());
if (result instanceof Integer) {
int num = (int) result;
Log.d(TAG, "num: " + num);
mResultTextView.setText((num == -1) ? "Not recognized." : String.valueOf(num));
}
//资源回收
if (mClient != null) {
mClient.release();
}
</code></pre>
<h2>四、加入我们</h2>
<p>帮助AI在终端落地,开源AoE集成运行环境是我们走出的第一步!未来,为终端的开发者提供更多推理框架的支持,提供更多有价值的特性,是我们不懈追求的目标。如果您对这个项目感兴趣,如果您在终端AI运行环境方面有想法,如果您在使用时有疑问,诚挚邀请您加入我们。</p>
<h3>github地址:</h3>
<p><img src="/img/bVbwnBn?w=241&h=241" alt="图片描述" title="图片描述"><br>您的每一个Star都是对我们最大的肯定:)</p>
<h3>QQ交流群:</h3>
<p><img src="/img/bVbwnBr?w=247&h=248" alt="图片描述" title="图片描述"><br>您的每一个问题都会帮我们成为更好的自己:)<br>QQ群号: 815254379</p>
<p>同时,也欢迎您关注滴滴技术公众号,我们会及时发布最新的开源信息和技术干货!</p>
<h3>滴滴技术公众号:</h3>
<p><img src="/img/bVbwnBy?w=1280&h=370" alt="图片描述" title="图片描述"></p>
Go-Spring : Another Go Style!
https://segmentfault.com/a/1190000020045075
2019-08-12T15:08:28+08:00
2019-08-12T15:08:28+08:00
滴滴技术
https://segmentfault.com/u/didijishu
16
<p>Go-Spring 是模仿 Java 的 Spring 全家桶实现的一套 GoLang 的应用程序框架,仍然遵循“习惯优于配置”的原则,提供了依赖注入、自动配置、开箱即用、丰富的第三方类库集成等功能,能够让程序员少写很多的样板代码。</p>
<p><strong>1.</strong><br><strong>前言</strong></p>
<p>去年年底的时候,我所在的团队由于业务调整,技术栈也随之发生改变,由之前的 PHP +Java 变成了 Golang + Java。初次接触 Golang,颇不适应,首先就是它那不同一般的语法,然后是没有一个成熟好用的开发框架。语法问题时间长了代码写的多了也就慢慢适应了,但是没有顺手的开发框架就太影响开发效率和代码质量了,作为一个资深的 Java + Spring 全家桶开发者,我希望能改变这一现状。经过一段时间的使用和探索,我发现完全可以搞出一套像 Spring 全家桶(Spring Framework + Spring Boot +Spring Cloud)那样的解决方案出来!</p>
<p>Spring 全家桶在 Java 世界的地位自然无需多言,它不仅为 Java 开发者证明了基于注解开发、基于 AOP 开发以及面向接口开发能够给程序带来极大的灵活性,更重要的是带来了依赖注入、声明式事务、统一的异常处理、模块自动化加载、更简单的 Maven 管理、更简单的单元测试等优秀的开发实践。</p>
<p>但是 GoLang 和 Java 毕竟不同,我为什么笃信自己肯定能搞出来呢?要回答这个问题,实际上是在回答另一个问题,即 Java 的哪些语言特性支撑了 Spring 全家桶能够实现那些核心能力,而 GoLang 又有哪些相似的语言特性?</p>
<p>追根溯源,Java 的字节码、反射、注解、包扫描等机制支撑了 Spring 全家桶能够实现 AOP 开发、依赖注入、声明式事务、模块自动化加载等核心特性。GoLang 因为没有字节码,所以不能实现 AOP 。但是 GoLang 有 Tags、Reflection、_ Imports、init() 机制,所以尽管实现起来不一定有 Java 优雅,但是也能实现依赖注入、模块自动化加载这些 Spring 全家桶的最核心特性。而且,尽管 GoLang 无法实现 AOP,但是也可以通过 Middleware 实现同样的功能。</p>
<p>经过一番探索和实践,终于 Go-Spring 诞生了!在我的眼中,Go-Spring 和 GoLang 本身一样,一出生就带着叛逆和创新精神,GoLang 以不同于主流编程语言语法的姿态出现,而 Go-Spring 则在质疑中以面向接口和依赖注入等多种绝对 Java 的特性出现在大家眼前。</p>
<p><strong>2.</strong><br><strong>特性</strong><br>Go-Spring 是模仿 Java 的 Spring 全家桶实现的一套 GoLang 的应用程序框架,仍然遵循“习惯优于配置”的原则,提供了依赖注入、自动配置、开箱即用、丰富的第三方类库集成等功能,能够让程序员少写很多的样板代码。总结起来,Go-Spring 至少有以下五大特点:</p>
<p><strong>▍可扩展的启动器框架,帮你优雅的组织代码</strong></p>
<p>下面这张图展示了一个 rtmp 服务器的启动函数,这里只截取了其中的一部分,可以看到启动函数的代码太长了,而且需要精心组织才能保证代码的可读性。</p>
<p><img src="/img/bVbwgLA?w=864&h=754" alt="clipboard.png" title="clipboard.png"></p>
<p>而使用 Go-Spring 的启动器框架则可以把这些启动过程封装到单独的文件中,使得功能更内聚,代码更清晰。下图展示的就是一个封装好的启动文件。 </p>
<p><img src="/img/bVbwgLB?w=864&h=485" alt="clipboard.png" title="clipboard.png"></p>
<p>在使用了 Go-Spring 的启动器框架之后,程序的启动过程就变成了非常简单的一行代码了!</p>
<p><img src="/img/bVbwgLD?w=864&h=339" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>▍面向接口+依赖注入,灵活替换实现方案</strong></p>
<p>Go-Spring 为 Redis 服务提供了统一的 API 接口,但是底层实现却有多种方案。用户在使用 Redis 服务编写业务代码时只需要关注 API 接口,而不需要关心底层采用的是哪种方案。</p>
<p><img src="/img/bVbwgLJ?w=864&h=710" alt="clipboard.png" title="clipboard.png"></p>
<p>当然用户最终会选择一个 redis 服务的底层实现,而引入这个实现仅仅只需要一行代码即可!</p>
<p><img src="/img/bVbwgLK?w=864&h=256" alt="clipboard.png" title="clipboard.png"></p>
<p>如果你想换成其他的 redis 底层实现,也仅仅是一行代码的事。</p>
<p><img src="/img/bVbwgLL?w=864&h=252" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>▍自动绑定配置项,简化配置文件使用</strong></p>
<p>在使用了 Go-Spring 的代码中只需要为变量设置好要绑定的配置项的名称,并在配置文件中添加该配置项,Go-Spring 就会自动帮你完成变量和配置项的绑定工作。</p>
<p><img src="/img/bVbwgMb?w=864&h=289" alt="clipboard.png" title="clipboard.png"></p>
<p>Go-Spring 还支持按照运行环境绑定不同的配置文件,比如当检测到线下环境时 Go-Spring 使用 application-test.properties 配置文件,而当检测到线上环境时会使用 application-online.properties 配置文件。</p>
<p><img src="/img/bVbwgMg?w=864&h=356" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>▍有效地帮助做好项目的依赖管理</strong></p>
<p>Go-Spring 为每一个模块都提供了一个抽象接口,使用者不需要关心接口内部是怎么实现的,这样能非常容易的解决依赖升级的问题。Go-Spring 保证已发布的所有版本的项目依赖都是正确的,而且 Go-Spring 每发布一个版本都会对依赖进行升级,这样使用者只需要关注 Go-Spring 的版本变化,就能享受到其他依赖自动升级的好处!</p>
<p><strong>▍让复杂的单元测试变得更简单</strong></p>
<p>GoLang 的单元测试尤其对 http 的单元测试简直烂的要命!使用 Go-Spring 启动的项目能够在单元测试的时候使用真实的项目运行环境,而不是使用一个 fake 的 http 环境。</p>
<p><strong>3.</strong><br><strong>组件</strong></p>
<p>Go-Spring 包含了四个核心项目,其中</p>
<p><img src="/img/bVbwgMj?w=725&h=395" alt="clipboard.png" title="clipboard.png"></p>
<p>go-spring 实现了 IoC 容器和依赖注入等核心功能;</p>
<p>go-spring-boot 提供了自动配置及应用程序的启动框架;</p>
<p>go-spring-cloud 立足开源世界打造人人可用的微服务框架;</p>
<p>go-spring-didi 聚焦滴滴内部技术实现具有滴滴特点的微服务框架。</p>
<p><strong>4.</strong><br><strong>示例</strong></p>
<p>下面我将通过一个最简单的 http 服务为大家展示如何使用 Go-Spring。</p>
<ol><li>新建 main.go 文件,创建启动程序,并且指定配置文件所在目录。</li></ol>
<p><img src="/img/bVbwgMm?w=1080&h=341" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>在程序中引入 echo http 服务。</li></ol>
<p><img src="/img/bVbwgMq?w=1080&h=348" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>新建 example.go 文件,实现一个示例服务,并且在 InitController() 函数中注册 http 接口的路由。</li></ol>
<p><img src="/img/bVbwgMy?w=1080&h=414" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>将一个示例服务的对象注册到 Go-Spring 的 IoC 容器里,这样 Go-Spring 就能自动地加载用户注册的 http 接口的路由。</li></ol>
<p><img src="/img/bVbwgMC?w=1080&h=397" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>在 main.go 文件中引入示例服务所在的包,这样 Go-Spring 框架在启动的时候就能加载示例服务所在的模块。</li></ol>
<p><img src="/img/bVbwgMH?w=1080&h=332" alt="clipboard.png" title="clipboard.png"></p>
<p>通过上面的 5 个步骤我们就得到了一个简单但完整的 http 服务,使用 go run main.go 命令启动程序,并使用 curl <a href="https://link.segmentfault.com/?enc=qx9EdAX1muLcvT3Elq9u%2BA%3D%3D.bRxD967cgp4T4R%2BGwRxXOMBWRbGaljdCe1k3e8oVyCk%3D" rel="nofollow">http://localhost</a>:8080/ 进行测试,可以看到请求的返回结果如下:</p>
<p>{"code":900001,"msg":"biz error"}</p>
<p>OK,是不是已经开始感觉到 Go-Spring 的威力了!下面我们再来看一下使用了 Go-Spring 框架的项目的单元测试会怎么写。</p>
<p>首先,我们可以编写一个 TestMain 函数用于启动真实的 http 服务器。</p>
<p><img src="/img/bVbwgMK?w=864&h=268" alt="clipboard.png" title="clipboard.png"></p>
<p>实际上图展示的代码还可以更精简,精简为一行。</p>
<p>然后,我们可以编写一个测试函数发送真实的 http 请求,也不需要 fake 或者 mock。</p>
<p><img src="/img/bVbwgMV?w=864&h=312" alt="clipboard.png" title="clipboard.png"></p>
<p>执行这个单元测试,你就会发现,你得到了一个完全不用 mock 和 fake ,并且功能完整、可以断点调试的测试环境。</p>
<p><strong>5.</strong><br><strong>总结</strong></p>
<p>除了上面展示的能够创建 http 服务和单元测试之外,Go-Spring 已经支持了 mysql 服务、redis 服务、kafka 服务、ddmq服务、服务注册和服务发现服务以及多种 rpc 服务,并且更多的新组件和新特性正在源源不断的通过滴滴内源加入进来,未来 Go-Spring 会变得越来越完善,越来越好用!</p>
<p>Another Go Style!我个人认为 Go-Spring 代表了一种新的编程模式,甚至是一种新的生产力方式,我希望大家在使用 Go-Spring 的过程中能够解放思想,提高效率,得到更多的快乐和自由,也多留一些时间给朋友和家人!</p>
<p>本文首发自普惠出行产品技术 (ID:pzcxtech)</p>
<p><img src="/img/bVbwgK6?w=594&h=172" alt="图片描述" title="图片描述"></p>
RN 技术探索:Hermes Engine 初探
https://segmentfault.com/a/1190000020044884
2019-08-12T14:56:57+08:00
2019-08-12T14:56:57+08:00
滴滴技术
https://segmentfault.com/u/didijishu
6
<p>自从 Google 的 Flutter 发布之后,Facebook 对 React-Native 的迭代开始快了起来,优化 React-Native 的性能表现,避免被 Flutter 比下去。最近一个比较大的动作是开源了一个 JavaScript 引擎,并将其包含到 React-Native 中。那么这款引擎它有什么不同,相比 V8、JSC 这些 JavaScript 引擎又有什么优势呢,现在本文来为你揭晓。</p>
<p><strong>1.</strong><br><strong>Hermes 引擎是什么,优势有哪些?</strong></p>
<p>重要的事情提前说:Hermes 引擎是 Facebook 研发,在 React-Native Android 端用于替换 JavaScript Core 的 JavaScript 引擎。Hermes 引擎的优势是适合移动端的轻量级 JavaScript 引擎,使用 aot 编译,可以减少 Android 端内存使用,减小安装包大小,提升执行效率。</p>
<p><strong>2.</strong><br><strong>什么是 JavaScript 引擎?</strong></p>
<p>JavaScript 引擎是一个专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器之中。</p>
<p><strong>3.</strong><br><strong>主流 JavaScript 引擎</strong></p>
<p>V8(Google)、JavaScriptCore(Apple)、SpiderMonkey(Firefox)</p>
<p><strong>4.</strong><br><strong>RN 中的 JavaScript 引擎</strong></p>
<p>Weex,Android:V8,iOS:JavaScriptCore</p>
<p>RN,Android:JavaScriptCore(Hermes、V8),iOS:JavaScriptCore(Apple 要求)</p>
<p>注:Hermes Engine在React-native 0.60.2 版本后支持</p>
<p><strong>5.</strong><br><strong>Hermes 的特色</strong></p>
<p>预编译字节码(引擎加载二进制代码效率高于运行JS脚本)</p>
<p>无 JIT 编译器(减小了引擎大小,优化内存占用,但直接运行 JS 脚本的性能差于 V8 和 JSC)</p>
<p>针对移动端的垃圾回收策略</p>
<p><strong>6.</strong><br><strong>优化原理</strong></p>
<p><img src="/img/bVbwgF3?w=1080&h=597" alt="clipboard.png" title="clipboard.png"></p>
<p>截取自code.fb.com</p>
<p>传统 JavaScript 引擎通常是以上图的模式完成代码执行的,编译阶段只完成 babel 转义和 minify 压缩,产物还是 JavaScript 脚本,解释与执行的任务都需要在运行时完成(如 V8 引擎,还会在运行时将 JavaScript 编译为本地机器码)很明显缺点就是在运行时需要边解释边执行,甚至需要占用系统资源执行编译任务。</p>
<p><img src="/img/bVbwgF5?w=1080&h=599" alt="clipboard.png" title="clipboard.png"><br>取自code.fb.com</p>
<p>Hermes 引擎使用了 aot 编译的方式,将解释和编译过程前置到编译阶段,运行时只完成机器码的执行,大大提高了运行效率。</p>
<p><strong>7.</strong><br><strong>已有项目接入 Hermes</strong></p>
<ul>
<li>升级 React-Native 及相关库升级(成本较小)</li>
<li>因为 React-Native 0.60.x 变更为依赖 AndroidX,所以 Android 项目需要使用 28 以上版本编译,适配<br> Android 高版本,且需要迁移到 AndroidX(成本较大)</li>
<li>修改 build.gradle,添加 Hermes 相关属性及依赖(成本较小)</li>
</ul>
<p><strong>8.</strong><br><strong>是否支持 CodePush?</strong></p>
<p>Hermes 引擎预编译后的产物与RN原方式相同,都是在 assets 文件夹下生成的 index.android.bundle 文件。RN 原方式中 index.android.bundle 是经过压缩的 JavaScript 脚本文件,Hermes 预编译后则是二进制文件。因为只有产物文件格式的区别,并没有修改原有JS Bundle 的加载方式,所以 CodePush 可以继续使用。</p>
<p>目前 code-push 的两种发布模式支持情况:</p>
<p><img src="/img/bVbwgHm?w=620&h=175" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>9.</strong><br><strong>调试效率</strong></p>
<p>Debug 模式下 Hermes 不开启预编译以支持 Hot Reload ,缺点是 Release 模式下所有Hermes 引擎优势都不存在,甚至因为无 JIT 导致性能还要差于原有引擎。但开发者模式并不追求性能,而更追求调试效率。</p>
<p>Debug 模式内置 libhermes-inspector.so ,支持 Chrome inspect 的使用,支持 DevTools 协议,比原有 RN 调试体验更佳(应用内代理,不能同步调试原生调用)</p>
<p><strong>10.</strong><br><strong>ES 标准支持</strong></p>
<p>Hermes 支持 ES6,紧跟最新的 JavaScript 规范。为了优化引擎大小,不支持 RN 程序中使用较少的语言特性,如本地 eval()。</p>
<p><strong>11.</strong><br><strong>性能调研</strong></p>
<p><strong>▍包大小分析</strong></p>
<p><img src="/img/bVbwgHz?w=1080&h=442" alt="clipboard.png" title="clipboard.png"><br>JSC 引擎 Release 包</p>
<p><img src="/img/bVbwgHC?w=1080&h=427" alt="clipboard.png" title="clipboard.png"><br>Hermes 引擎 Release 包</p>
<p>原包大小 20MB(JSC)<br>新包大小 18MB(Hermes)</p>
<p>包大小减小 2MB,整体减少 2MB / 20MB = 10%</p>
<p>分析具体包大小减小的原因可以发现,包内容两者只有 lib 大小和 assets 的大小存在差异。</p>
<p><img src="/img/bVbwgHH?w=1080&h=58" alt="clipboard.png" title="clipboard.png"><br>JSC 引擎 Release 包</p>
<p><img src="/img/bVbwgHN?w=1080&h=117" alt="clipboard.png" title="clipboard.png"><br>Hermes 引擎 Release 包</p>
<p>对比 lib 内容,发现大小差距主要是由 libjsc.so 和 libhermes.so 两者的差距导致的,即 Hermes 引擎的大小。</p>
<p><img src="/img/bVbwgHS?w=1080&h=139" alt="clipboard.png" title="clipboard.png"><br>JSC 引擎 Release 包</p>
<p><img src="/img/bVbwgH6?w=1080&h=138" alt="clipboard.png" title="clipboard.png"><br>Hermes 引擎 Release 包</p>
<p>对比 assets 内容,发现大小变化主要由 index.android.bundle ,即 JavaScript 打包产物引起,Hermes 模式下反而更大的原因是进一步编译为二进制代码。</p>
<p>两者影响叠加导致整体减小,包大小得到优化。(支持的平台越多,包体积优化效果越好)</p>
<p><strong>▍内存分析</strong></p>
<p>实验方法:在相同的业务页面稳定状态下通过 Memory Profiler 查看内存占用情况</p>
<p><img src="/img/bVbwgIO?w=1080&h=356" alt="clipboard.png" title="clipboard.png"><br>JSC 引擎 Release 包</p>
<p><img src="/img/bVbwgIN?w=1080&h=338" alt="clipboard.png" title="clipboard.png"><br>Hermes 引擎 Release 包</p>
<p>原包平均内存占用 210MB<br>新包平均内存占用 190MB</p>
<p>内存占用平均减小20MB以上,整体减小20MB / 210MB = 10%<br>分析 Profiler 数据可以发现,内存优化主要发生在 Code 内存区。</p>
<p><img src="/img/bVbwgIZ?w=310&h=466" alt="clipboard.png" title="clipboard.png"><br>JSC 引擎 Release 包</p>
<p><img src="/img/bVbwgI3?w=308&h=468" alt="clipboard.png" title="clipboard.png"><br>Hermes 引擎 Release 包</p>
<p>Google 官方文档中对内存 Code 区的描述:</p>
<p>Code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。</p>
<p>联系到上个章节中包大小分析中 libhermes.so 尺寸的减小,可以很容易想到,内存占用的减少就是因为 .so 对内存占用的减小。另外两者对 JavaScript 内存的占用也有细微差别,但是可以忽略不计。</p>
<p><strong>▍TTI性能</strong></p>
<p>TTI:Time to Interactive,用户可交互时间,启动到页面渲染完成并且可以正常响应用户的输入的时间,衡量用户体验的移动端指标。</p>
<p>React-Native Android 中主要是 Application onCreate 开始到 RN 组件渲染完成可交互的时间。</p>
<p>值得吐槽的是,在 iOS 版本的 Pref Monitor 中直接就包含了这个指标的显示,但是 Android 版本的 Pref Monitor 只有四个指标,且并没有 TTI 这一指标。</p>
<p>在 Android 平台上可以通过 RN 提供的 ReactFindViewUtil 类获取 RN 组件对应的原生组件,注册对应的渲染回调,在控件渲染完成时记录TTI结束时间。</p>
<p><img src="/img/bVbwgJb?w=1080&h=32" alt="clipboard.png" title="clipboard.png"><br>JSC 引擎 Release 包</p>
<p><img src="/img/bVbwgJd?w=1080&h=43" alt="clipboard.png" title="clipboard.png"><br>Hermes 引擎 Release 包</p>
<p>原包 TTI 829ms<br>新包 TTI 694ms</p>
<p>TTI 减少 135ms,整体减少 135ms / 829ms = 16%</p>
<p><strong>12.</strong><br><strong>总结</strong></p>
<p>面对 Flutter 的咄咄攻势,React-Native 终于做出了一些改变,Hermes 作为一款适合移动端的 JavaScript 引擎,确实有其性能优势,希望通过本文能够让你更加了解 Hermes。</p>
<p>本文首发自普惠出行产品技术 (ID:pzcxtech)</p>
<p><img src="/img/bVbwgK6?w=594&h=172" alt="图片描述" title="图片描述"></p>
滴滴开源DELTA:AI开发者可轻松训练自然语言模型
https://segmentfault.com/a/1190000019995961
2019-08-07T12:19:22+08:00
2019-08-07T12:19:22+08:00
滴滴技术
https://segmentfault.com/u/didijishu
3
<p>8月2日消息,自然语言处理领域顶级会议ACL2019在意大利弗洛伦萨继续召开。会上滴滴正式宣布开源基于深度学习的语音和自然语言理解模型训练平台DELTA,以进一步帮助AI开发者创建、部署自然语言处理和语音模型,构建高效的解决方案,助力NLP应用更好落地。</p>
<p>DELTA是滴滴第22个开源项目。自然语言处理模型和语音模型是很多AI系统与用户交互的接口,此次滴滴正式这一开源深度学习模型训练框架,旨在进一步降低开发者创建、部署自然语言处理系统和语音模型的难度。</p>
<p><img src="/img/bVbv31p?w=826&h=452" alt="clipboard.png" title="clipboard.png"><br>滴滴自然语言处理首席科学家Kevin Knight在ACL2019现场</p>
<p>DELTA主要基于TensorFlow构建,能同时支持NLP(自然语言处理)和语音任务及数值型特征的训练。整合了包括文本分类、命名实体识别、自然语言推理、问答、序列到序列文本生成、语音识别、说话人验证、语音情感识别等重要算法模型,形成一致的代码组织架构,整体包装统一接口。</p>
<p>用户准备好模型训练数据,并指定好配置Configuration,模型训练pipeline可以根据配置进行数据处理,并选择相应的任务和模型,进行模型训练。在训练结束之后,自动生成模型文件保存。该模型文件形成统一接口,可以直接上线使用,快速产品化,能让从研究到生产变得更容易。</p>
<p><img src="/img/bVbv31r?w=865&h=346" alt="clipboard.png" title="clipboard.png"></p>
<p>值得注意的是,除可支持多种模型的训练,DELTA还支持灵活配置,开发者可基于DELTA搭建成多达几十种的复杂的模型;此外,DELTA在多种常用任务上提供了稳定高效的benchmark,用户可以简单快速的复现论文中的模型的结果,同时也可以在此基础上扩展新的模型。在模型构建完成后,用户可以使用DELTA的部署流程工具,迅速完成模型上线。从论文到产品部署无缝衔接。</p>
<p>目前AI开发者可登陆Github(<a href="https://link.segmentfault.com/?enc=Qnl5bfsYibUHtvprkRJ%2BQg%3D%3D.UAccUpaTcf%2BAJpzba3wT9afFdxsVCiTPxSAQ1iryR7TloEcqYwv5zJpXKcZlokvynfrtY8us6wTat%2BNIyd%2BvoJC9xgY46ayFgSJ8keVBM7k4yVfloqJ43eqBl2skrY2Rei7APIFQhDH17piJsg1RxlsMNQ0iQmcqoP%2BNaFd3Exh7ua2vzC24ohrNLXy2k9ju" rel="nofollow">https://github.com/didi/delta...</a>,利用DELTA加快实验进度,部署用于文本分类、命名实体识别、自然语言推理、问答、序列到序列文本生成、语音识别、说话人验证、语音情感识别等任务的系统。用户亦可在滴滴的开源平台上(<a href="https://link.segmentfault.com/?enc=pfTbY91jItHRCoOcOKAPvw%3D%3D.Y9%2Bl9vr5BpzTqukJPwxZ5jrzGZGVkUneqa4QBKrZNnc%3D" rel="nofollow">https://didi.github.io/</a>)获取更多滴滴开源项目的相关信息。</p>
<p>实际上,NLP和语音技术在滴滴已经有广泛的应用。通过大量应用了包括自然语言处理、深度学习、知识图谱、语音、推荐等技术,滴滴自建了基于AI的智能客服系统,能利用人工智能技术辅助人工客服,提高人工客服处理问题的效率,并减少人工客服在重复、简单问题上的处理量。此外,基于语音识别以及自然语言理解技术,滴滴也在构建驾驶员语音助手,日本和澳洲的滴滴司机即将能用语音直接“免接触”接单。而在未来,这一语音助手也将支持全方位的语音交互服务,包括影音娱乐、信息查询、车内环境调节,到乘客通信、客服,甚至是加油、充电或维保服务。与此同时,滴滴也在积极推进相关能力的开放,通过提供一站式自然语言处理工具、一站式机器人开放平台,帮助行业合作伙伴更好地实现AI应用落地。</p>
滴滴助力全球首个 DevOps 标准建设
https://segmentfault.com/a/1190000019430138
2019-06-10T12:13:40+08:00
2019-06-10T12:13:40+08:00
滴滴技术
https://segmentfault.com/u/didijishu
0
<p><strong>出品 | 滴滴技术</strong></p>
<p><img src="/img/bVbtydS?w=2350&h=1000" alt="图片描述" title="图片描述"></p>
<p>前言:日前,全球首个 DevOps 标准,即《研发运营一体化 (DevOps) 能力成熟度模型》标准已经发布。该标准由工信部直属单位中国信息通信研究院牵头,云计算开源产业联盟、高效运维社区和 DevOps 时代社区联合发起,滴滴等国内外互联网领先企业共同编制。</p>
<p><img src="/img/bVbtyd4?w=1280&h=720" alt="图片描述" title="图片描述"></p>
<p>2019年4月12日在 GOPS 全球运维大会 2019 深圳站上,工信部中国信息通信研究院云计算产业联盟(OSCAR 联盟)正式授予滴滴为 DevOps 标准工作组成员单位,这标志着滴滴研发运营一体化能力已经获得了行业权威机构组织的认可,更是成为滴滴助力全球 DevOps 标准化的重要里程碑。</p>
<p><img src="/img/bVbtyef?w=1280&h=720" alt="图片描述" title="图片描述"><br>△ 授牌仪式</p>
<p>DevOps 标准由 OSCAR 联盟、高效运维社区和 DevOps 时代社区联合发起,联合了华为、百度、阿里、腾讯、滴滴等行业顶级公司的百余位专家,共同编写而成。作为全球首个 DevOps 标准,《研发运营一体化(DevOps)能力成熟度模型》已在联合国直属标准化组织 ITU-T、中国工信部、中国通信标准化协会正式立项为国际标准(立项行标号为:2018-1753T-YD 等)。</p>
<p><img src="/img/bVbtyeS?w=1280&h=720" alt="图片描述" title="图片描述"></p>
<p>△ 大会现场</p>
<p>滴滴效能平台部(EP)代表公司受邀参与了标准的制定。在本次标准制定过程中,效能平台部多位 DevOps 领域资深专家在敏捷开发、持续交付、系统和工具等核心章节深度参与了讨论和编写,贡献了滴滴多年来在软件研发领域积累的实践成果。</p>
<p><img src="/img/bVbtye1?w=1280&h=720" alt="图片描述" title="图片描述"></p>
<p>△ 滴滴资深 DevOps 专家杨永强现场分享</p>
<p>源于在软件领域先进的管理理念和多年的工程实践,效能平台部一直以打造全面的 DevOps 能力为核心,为业务提供了从需求到发布的研发全生命周期工具支撑,打造端到端研发协同平台,助力业务成功。<br><img src="/img/bVbtye8?w=1920&h=1080" alt="图片描述" title="图片描述"></p>
<p>△ OE 打通了各个环节</p>
<p>依托滴滴自身丰富的 DevOps 实践,效能平台部自研的一站式研发平台 OE(OneExperience),打通了需求-开发-构建-测试-部署-发布各个环节,有效的支撑整个滴滴研发人员的日常开发工作,为他们提供高效的研发体验,并大幅缩短从开发到上线的周期,确保整个研发流程透明可控,具备完善的研发管理数据驱动能力。</p>
<p><img src="/img/bVbtyfs?w=1280&h=720" alt="图片描述" title="图片描述"></p>
<p>△ EP 部分团队代表</p>
<p>滴滴效能平台部负责人蔡晓鸥表示:“滴滴通过共建 DevOps 标准和生态,旨在将公司工程效能领域优秀的工具和实践输出,指导行业更好地实践 DevOps 理念。未来滴滴将持续加大在 DevOps 领域的投入,和业内顶级公司一起推动行业工程效能水平的提升。”</p>
<p>▍END</p>
<p><img src="/img/bVbtGPr?w=705&h=232" alt="图片描述" title="图片描述"></p>
<p>滴滴效能平台部(Effectiveness Platform)成立于2015年4月28日,致力于通过技术持续提升组织效能。效能平台部肩负着企业信息化建设、研发工具体系建设等重任,产品服务覆盖全公司各个领域。通过工具系统的建设提升公司的管理效率、协同效率、研发效率。我们的愿景是成为高效能组织的塑造者,助力企业实现智能工作,美好协同。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
滴滴宋世君:DS(数据分析师),究竟是做什么的?
https://segmentfault.com/a/1190000019380924
2019-06-04T10:47:01+08:00
2019-06-04T10:47:01+08:00
滴滴技术
https://segmentfault.com/u/didijishu
1
<p><strong>出品 | 滴滴技术</strong><br><strong>作者 | 宋世君</strong></p>
<p><img src="/img/bVbttUy?w=2350&h=1000" alt="图片描述" title="图片描述"></p>
<p>前言:本文的作者是滴滴出行数据科学部负责人宋世君,曾在 Facebook 、Google 核心部门就职,是知名的华人数据分析总监。经世君老师授权在此分享给大家,希望让大家理解数据分析师的背后——数据对于一个产品的核心价值,无论把握数据的是DS,还是研发、产品同学。希望能够帮到你。</p>
<p>DS 在市场上是近些年出现的一个新的职能,比起研发、算法、产品、运营等等这些已经演进二三十年的职能,我们还是在非常年轻的阶段。</p>
<p>一方面,从市场上人才的供需关系可以看出来这个职能的发展和需求,但是另一方面,和任何新事物一样,这个新的职能也有很多挑战,今天我想谈谈我怎么看待 DS 这个职能,和我们的发展方向。</p>
<p>首先,我们要明确 DS 并不是一个公司的”必要”职能,但是在一个公司的发展壮大过程中又会有 DS 出现的必然性和存在的合理性。我们就像一把枪上的准星,没有准星也能开枪,但是准星能使这把枪更加有用。公司没有任何人做数据分析,短期也依然能运行,只是很多地方运行地会不太好;如果有一天公司里做数据分析的人都消失了,公司短时间内也不会垮掉,但是时间长一些肯定会有影响。</p>
<p>当我们不是”必要”职能的时候,我们就要问自己“DS 是谁”、“DS 做什么”、“DS 存在的价值是什么”、 “DS 要往哪个方向发展”?</p>
<p><strong>▎DS 是谁</strong></p>
<p>用心理学的术语,这个其实是 DS 的“本我”。我们是一群在相关量化领域受过专业的训练,并且希望应用自己的量化能力,在数据中挖掘对业务有用的信息,并且通过这些信息为业务发展提供助力但是同时又保持数据的中立性的人。</p>
<p>一个职能(或者说公司里的一个岗位)是由他应该做什么决定的,而不是由他正在做什么决定的。所以,我们描述 DS ,更多的是从我们自己觉得我们应该做什么,而不是我们现状做什么。比如很多同学有这样的疑问” DS 做大量取数的事情”,甚至很多业务合作方”期待我们满足很多取数的需求”。这些都与 DS 是谁无关,这只能说明我们还没有做好我们的工作,还有很多地方需要努力 ( 后面会展开谈 )。</p>
<p>从个体的角度,这也意味着我们看待 DS 并不是看这个人的学术专业,而是看这个人的动机和意愿。公司里跟数据有关的职能是多样的,有些是把数据作为拿到业务结果的抓手。要对业务结果负责,这些是数据运营。有些是把数据作为研发的对象,对跟数据相关的这些产品负责,这些是工程研发。有些是基于数据做实时地在线实现,这些是算法工程师的工作。</p>
<p>这些都是我们的合作伙伴, 但是我们又有我们自己的定位, 跟这些都不同. 我们应该为我们工作的中立性和科学性负责. 我们需要有业务的思想, 但是我们并不是要做业务本身, 我们希望做业务发展的催化剂。</p>
<p><strong>▎DS 做什么</strong></p>
<p>我总结我们做的事情,可以抽象成三类 (1) 描述现状 (2) 寻找规律 (3) 推动改进。这三类事是逐层推进地,但是都很重要。</p>
<p>DS 首先要描述现状,也就是我们常说的 “数数”。</p>
<p>当我们连客观现状都描述不清楚的时候,是谈不上寻找规律和推动改进的。我们工作中大量的取数工,我们做指标,做数据报表看板等等都是在这一类之中。但是为什么很多同学对 “取数”工作有很大疑惑或者是觉得没有成就感呢?我觉得这是因为我们在被动地取数,或者说我们并没有把取数本身和自己业务的主线联系起来,而仅仅是在做填空题。</p>
<p>另外,我在数据分析十条中提到 “分析什么问题,往往比用什么方法更重要”,应用在取数上就是 “取什么数”、“为什么取”往往比 “怎么取”、“是多少”更重要。很多时候,从业务角度思考 “为什么取”就能给我们更强的价值感,如果能主动去思考 “为什么取”,则更加会有参与感。虽然这是第一步,但是价值是极大的,如果不能帮助公司描述现状,公司就是在盲目前进。这第一步就要求我们的每位同学有独立思考尤其是批判性思考的能力。</p>
<p><img src="/img/bVbttYf?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>DS 还要<strong>寻找规律</strong>。</p>
<p>数据分析的本质就是要寻找规律,寻找那些数据信息中隐含,但是别人还没发现的规律。我们常说的统计推断、因果关系、增长推动、预测建模、实验评估等等都是在寻找规律。这些规律就是我们常说的 “洞见”。</p>
<p>当然, 有含金量的规律是不容易发现的, 这也正是我们 DS 存在的价值. 如果我们能看到的规律大家都能看到,那么我们就没有提供价值;谁能挖掘的深, 谁能看到更本质的规律, 谁就提供了更大的价值,所以我们的学术训练、科学方法、实践经验、数据敏感度等等都是在帮我们发现别人看不到的价值。所以我鼓励大家在描述自己的工作的时候,出发点不应该是我用了什么方法,而是我发现了什么规律 ( 洞见 )。这要求我们的每位同学有<strong>很强的好奇心和坚定的信念</strong>。</p>
<p>我们描述现状和寻找规律,最终的目的都是为了<strong>推动改进</strong>,这也就是我们常说的影响。我总结过DS的影响可以分成四类: (1) 改善重要指标 (2) 影响产品决策 (3) 影响操作流程 (4) 创造可持续解决方案。</p>
<p>如果我们做了一些事,但是没有直接或间接地实现这四类里面的任何一类,那我们要反过来思考下我们是不是把时间花在了正确的地方。以及我们以后要怎么做,才能让我们的单位时间投入产出最大化?更理想的情况,是在做事之前,先想想 ( 如果是被动需求的话,问问需求方 ) 我们要做的事会在哪些方面产生影响。要实现这些影响,还要求我们的每位同学有<strong>同理心和业务 (产品/运营/市场等)思维</strong>,同时还要有精炼的能力,优秀的沟通技巧,说服的能力。理解了我们影响力可以发挥的四个维度,也就解释了“ DS 存在的价值是什么”。从心理学的概念,这相当于 DS 的 “超我”。</p>
<p><strong>▎DS 要往哪个方向发展</strong></p>
<p>这相当于是DS “自我”的问题。</p>
<p>我把这个问题总结成两个方面 “能力建设”和 “文化建设”。在<strong>能力建设</strong>方向,打铁还需自身硬。我们要有能力做更加深入的分析,应用更加科学的工具,让别人做不了的东西我们能做,别人看不到的规律我们能看到。这里要强调一点,就是能力不光是技术能力,还有业务思考的能力。我们组织 Delta 计划就是为了帮助同学们提高这种能力。我们也鼓励大家多通过行程学习小组、轮岗、和团队里的资深专家交流的方式。提高自己的能力。同时,我们也鼓励大家多站在业务的角度,思考数据能发挥什么作用,。多从各业务 leader 那里学习他们的思维方式和角度,然后结合我们的数据积累形成我们自己的东西。</p>
<p>跟能力建设同等重要甚至更重要的是<strong>文化建设</strong>。我们改变环境 ( 同事、公司、行业 ) 怎么看待 DS ,首先要坚定我们自己怎么看待自己。这里有自信的问题。我们的价值是由我们做的事情决定的 ( 自我 ) ,这个并不依赖于外界对我们的认知和肯定;我们要提高自己的价值,本质上也是如何让自己做的事情更有价值。有了自信,我们才能有方向去引导我们的合作同事怎么看待我们,怎么知道我们能做什么更有意义,别人怎么看待自己,本质上反应地是自己怎么看待自己。如果我们自己就觉得自己应该取数,那在别人眼里就是取数。如果我们告诉别人,我们的时间用在其他 ( 更有价值 ) 的地方对业务帮助更大,那么我们和对方都有意愿去这样做。而我们通过努力能够兑现这些,会让对方更加认定我们这个定位,形成正反馈。</p>
<p>大家在DS团队遇上的问题,我若干年前在 Google 和 Facebook 都遇上过,但是通过我们整体团队的努力,逐步证明自己,在市场上树立了 DS 的品牌和认知,并被市场上认定为这个职能的标杆。DS 作为一个职能,也获得跟工程、产品相类似的地位,近期多次被评为最有前景的工作。这个过程是逐步的,是需要时间的,也需要我们一起努力。</p>
<p>我们在滴滴其实也是在做这么一件事,DS 和数据驱动的理念在中国发展尚早,很多事情还停留在理论和感性层面,相当于硅谷若干年前的状态。这也是为什么我们这个部门的同学面临这么大的迷茫,而我们这些 leader 要帮助大家坚定方向,因为我们是市场上引领这个职能的一群人,在探索和拓展着这个职能的边界,而这个过程注定是有挑战的。和其他职能不同,我们的各位 leader 和基层同学, 在做具体事情的同时,还在创造着这个职能的历史。</p>
<p><strong>▎数据分析十条</strong></p>
<p>最后,我还想重新提一下我总结过的 “数据分析十条”,上面讲的很多方面都在这十条里面有反映:</p>
<ol>
<li>分析师的核心能力是思辨 [DS做什么]</li>
<li>对讲真话负责,保持中立 [DS是谁]</li>
<li>论据充分,论证严谨,观点简明 [推动改进]</li>
<li>数据先于观点,而不是观点先于数据 [DS做什么]</li>
<li>不要把问题复杂化,也不要惧怕复杂度 [DS是谁, DS做什么]</li>
<li>分析什么问题,往往比用什么方法更重要 [描述现状]</li>
<li>好的分析师给别人输入,而不只是帮别人输出 [文化建设]</li>
<li>分析没有什么价值,除非洞见改变了什么其他的东西 [非必要职能]</li>
<li>如果可能应该基于问题收集数据,而不只是基于数据来问问题 [本文未提]</li>
<li>不是所有问题都可以分析出答案,以开放的心态采纳其他的观点 [本文未提]</li>
</ol>
<p><strong>▍END</strong><br><img src="/img/bVbttZ1?w=728&h=414" alt="图片描述" title="图片描述"></p>
<p>资深数据分析行业带头人,前 Facebook 数据分析总监,Instagram 数据分析负责人,Google长尾广告增长负责人,在 Google 和 Facebook 的客户端、广告、社交网络内容生产与消费等方面的数据应用有着广泛的经验。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
曹春晖:谈一谈 Go 和 Syscall
https://segmentfault.com/a/1190000019375999
2019-06-03T18:29:50+08:00
2019-06-03T18:29:50+08:00
滴滴技术
https://segmentfault.com/u/didijishu
9
<p>出品 | 滴滴技术<br>作者 | 曹春晖</p>
<p><img src="/img/bVbtsFm?w=2350&h=1000" alt="图片描述" title="图片描述"></p>
<p>前言:syscall 是语言与系统交互的唯一手段,理解 Go 语言中的 syscall,本文可以帮助读者理解 Go 语言怎么与系统打交道,同时了解底层 runtime 在 syscall 优化方面的一些小心思,从而更为深入地理解 Go 语言。</p>
<p><strong>▎阅读索引</strong></p>
<ul>
<li>概念</li>
<li>入口</li>
<li>系统调用管理</li>
<li>runtime 中的 SYSCALL</li>
<li>和调度的交互</li>
</ul>
<ol>
<li>entersyscall</li>
<li>exitsyscallfast</li>
<li>exitsyscall</li>
<li>entersyscallblock</li>
<li>entersyscallblock_handoff</li>
<li>entersyscall_sysmon</li>
<li>entersyscall_gcwait</li>
</ol>
<ul><li>总结</li></ul>
<p><strong>▎概念</strong></p>
<p><img src="/img/bVbtsGm?w=1588&h=1244" alt="图片描述" title="图片描述"></p>
<p><strong>▎入口</strong></p>
<p>syscall 有下面几个入口,在 syscall/asm_linux_amd64.s 中。</p>
<pre><code>1 func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
2
3 func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
4
5 func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
6
7 func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
8 </code></pre>
<p>这些函数的实现都是汇编,按照 linux 的 syscall 调用规范,我们只要在汇编中把参数依次传入寄存器,并调用 SYSCALL 指令即可进入内核处理逻辑,系统调用执行完毕之后,返回值放在 RAX 中:</p>
<p><img src="/img/bVbtsGA?w=505&h=78" alt="图片描述" title="图片描述"></p>
<p>Syscall 和 Syscall6 的区别只有传入参数不一样:</p>
<pre><code> 1 // func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
2TEXT ·Syscall(SB),NOSPLIT,$0-56
3 CALL runtime·entersyscall(SB)
4 MOVQ a1+8(FP), DI
5 MOVQ a2+16(FP), SI
6 MOVQ a3+24(FP), DX
7 MOVQ $0, R10
8 MOVQ $0, R8
9 MOVQ $0, R9
10 MOVQ trap+0(FP), AX // syscall entry
11 SYSCALL
12 // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 转无符号,http://lxr.free-electrons.com/source/include/linux/err.h#L17
13 CMPQ AX, $0xfffffffffffff001
14 JLS ok
15 MOVQ $-1, r1+32(FP)
16 MOVQ $0, r2+40(FP)
17 NEGQ AX
18 MOVQ AX, err+48(FP)
19 CALL runtime·exitsyscall(SB)
20 RET
21ok:
22 MOVQ AX, r1+32(FP)
23 MOVQ DX, r2+40(FP)
24 MOVQ $0, err+48(FP)
25 CALL runtime·exitsyscall(SB)
26 RET
27
28// func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
29TEXT ·Syscall6(SB),NOSPLIT,$0-80
30 CALL runtime·entersyscall(SB)
31 MOVQ a1+8(FP), DI
32 MOVQ a2+16(FP), SI
33 MOVQ a3+24(FP), DX
34 MOVQ a4+32(FP), R10
35 MOVQ a5+40(FP), R8
36 MOVQ a6+48(FP), R9
37 MOVQ trap+0(FP), AX // syscall entry
38 SYSCALL
39 CMPQ AX, $0xfffffffffffff001
40 JLS ok6
41 MOVQ $-1, r1+56(FP)
42 MOVQ $0, r2+64(FP)
43 NEGQ AX
44 MOVQ AX, err+72(FP)
45 CALL runtime·exitsyscall(SB)
46 RET
47ok6:
48 MOVQ AX, r1+56(FP)
49 MOVQ DX, r2+64(FP)
50 MOVQ $0, err+72(FP)
51 CALL runtime·exitsyscall(SB)
52 RET</code></pre>
<p>两个函数没什么大区别,为啥不用一个呢?个人猜测,Go 的函数参数都是栈上传入,可能是为了节省一点栈空间。。在正常的 Syscall 操作之前会通知 runtime,接下来我要进行 syscall 操作了 runtime·entersyscall ,退出时会调用 runtime·exitsyscall 。</p>
<pre><code> 1 // func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
2TEXT ·RawSyscall(SB),NOSPLIT,$0-56
3 MOVQ a1+8(FP), DI
4 MOVQ a2+16(FP), SI
5 MOVQ a3+24(FP), DX
6 MOVQ $0, R10
7 MOVQ $0, R8
8 MOVQ $0, R9
9 MOVQ trap+0(FP), AX // syscall entry
10 SYSCALL
11 CMPQ AX, $0xfffffffffffff001
12 JLS ok1
13 MOVQ $-1, r1+32(FP)
14 MOVQ $0, r2+40(FP)
15 NEGQ AX
16 MOVQ AX, err+48(FP)
17 RET
18ok1:
19 MOVQ AX, r1+32(FP)
20 MOVQ DX, r2+40(FP)
21 MOVQ $0, err+48(FP)
22 RET
23
24// func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
25TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
26 MOVQ a1+8(FP), DI
27 MOVQ a2+16(FP), SI
28 MOVQ a3+24(FP), DX
29 MOVQ a4+32(FP), R10
30 MOVQ a5+40(FP), R8
31 MOVQ a6+48(FP), R9
32 MOVQ trap+0(FP), AX // syscall entry
33 SYSCALL
34 CMPQ AX, $0xfffffffffffff001
35 JLS ok2
36 MOVQ $-1, r1+56(FP)
37 MOVQ $0, r2+64(FP)
38 NEGQ AX
39 MOVQ AX, err+72(FP)
40 RET
41ok2:
42 MOVQ AX, r1+56(FP)
43 MOVQ DX, r2+64(FP)
44 MOVQ $0, err+72(FP)
45 RET
</code></pre>
<p>RawSyscall 和 Syscall 的区别也非常微小,就只是在进入 Syscall 和退出的时候没有通知 runtime,这样 runtime 理论上是没有办法通过调度把这个 g 的 m 的 p 调度走的,所以如果用户代码使用了 RawSyscall 来做一些阻塞的系统调用,是有可能阻塞其它的 g 的,下面是官方开发的原话:</p>
<p><em>Yes, if you call RawSyscall you may block other goroutines from running. The system monitor may start them up after a while, but I think there are cases where it won't. I would say that Go programs should always call Syscall. RawSyscall exists to make it slightly more efficient to call system calls that never block, such as getpid. But it's really an internal mechanism.</em></p>
<pre><code> 1 // func gettimeofday(tv *Timeval) (err uintptr)
2 TEXT ·gettimeofday(SB),NOSPLIT,$0-16
3 MOVQ tv+0(FP), DI
4 MOVQ $0, SI
5 MOVQ runtime·__vdso_gettimeofday_sym(SB), AX
6 CALL AX
7
8 CMPQ AX, $0xfffffffffffff001
9 JLS ok7
10 NEGQ AX
11 MOVQ AX, err+8(FP)
12 RET
13 ok7:
14 MOVQ $0, err+8(FP)
15 RET</code></pre>
<p><strong>▎系统调用管理</strong></p>
<p>先是系统调用的定义文件:</p>
<pre><code>1/syscall/syscall_linux.go</code></pre>
<p>可以把系统调用分为三类:</p>
<p>阻塞系统调用<br>非阻塞系统调用<br>wrapped 系统调用</p>
<p>阻塞系统调用会定义成下面这样的形式:</p>
<pre><code>1 //sys Madvise(b []byte, advice int) (err error)</code></pre>
<p>然后,根据这些注释,mksyscall.pl 脚本会生成对应的平台的具体实现。mksyscall.pl 是一段 perl 脚本,感兴趣的同学可以自行查看,这里就不再赘述了。</p>
<p>看看阻塞和非阻塞的系统调用的生成结果:</p>
<pre><code> 1 func Madvise(b []byte, advice int) (err error) {
2 var _p0 unsafe.Pointer
3 if len(b) > 0 {
4 _p0 = unsafe.Pointer(&b[0])
5 } else {
6 _p0 = unsafe.Pointer(&_zero)
7 }
8 _, _, e1 := Syscall(SYS_MADVISE, uintptr(_p0), uintptr(len(b)), uintptr(advice))
9 if e1 != 0 {
10 err = errnoErr(e1)
11 }
12 return
13}
14
15func EpollCreate(size int) (fd int, err error) {
16 r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE, uintptr(size), 0, 0)
17 fd = int(r0)
18 if e1 != 0 {
19 err = errnoErr(e1)
20 }
21 return
22}</code></pre>
<p>显然,标记为 sys 的系统调用使用的是 Syscall 或者 Syscall6,标记为 sysnb 的系统调用使用的是 RawSyscall 或 RawSyscall6。</p>
<p>wrapped 的系统调用是怎么一回事呢?</p>
<pre><code>1func Rename(oldpath string, newpath string) (err error) {
2 return Renameat(_AT_FDCWD, oldpath, _AT_FDCWD, newpath)
3}</code></pre>
<p>可能是觉得系统调用的名字不太好,或者参数太多,我们就简单包装一下。没啥特别的。</p>
<p><strong>▎runtime 中的 SYSCALL</strong></p>
<p>除了上面提到的阻塞非阻塞和 wrapped syscall,runtime 中还定义了一些 low-level 的 syscall,这些是不暴露给用户的。</p>
<p>提供给用户的 syscall 库,在使用时,会使 goroutine 和 p 分别进入 Gsyscall 和 Psyscall 状态。但 runtime 自己封装的这些 syscall 无论是否阻塞,都不会调用 entersyscall 和 exitsyscall。虽说是 “low-level” 的 syscall。</p>
<p>不过和暴露给用户的 syscall 本质是一样的。这些代码在 runtime/sys_linux_amd64.s中,举个具体的例子:</p>
<pre><code>1TEXT runtime·write(SB),NOSPLIT,$0-28
2 MOVQ fd+0(FP), DI
3 MOVQ p+8(FP), SI
4 MOVL n+16(FP), DX
5 MOVL $SYS_write, AX
6 SYSCALL
7 CMPQ AX, $0xfffffffffffff001
8 JLS 2(PC)
9 MOVL $-1, AX
10 MOVL AX, ret+24(FP)
11 RET
12
13TEXT runtime·read(SB),NOSPLIT,$0-28
14 MOVL fd+0(FP), DI
15 MOVQ p+8(FP), SI
16 MOVL n+16(FP), DX
17 MOVL $SYS_read, AX
18 SYSCALL
19 CMPQ AX, $0xfffffffffffff001
20 JLS 2(PC)
21 MOVL $-1, AX
22 MOVL AX, ret+24(FP)
23 RET</code></pre>
<p>下面是所有 runtime 另外定义的 syscall 列表:</p>
<pre><code> 1 #define SYS_read 0
2 #define SYS_write 1
3 #define SYS_open 2
4 #define SYS_close 3
5 #define SYS_mmap 9
6 #define SYS_munmap 11
7 #define SYS_brk 12
8 #define SYS_rt_sigaction 13
9 #define SYS_rt_sigprocmask 14
10 #define SYS_rt_sigreturn 15
11 #define SYS_access 21
12 #define SYS_sched_yield 24
13 #define SYS_mincore 27
14 #define SYS_madvise 28
15 #define SYS_setittimer 38
16 #define SYS_getpid 39
17 #define SYS_socket 41
18 #define SYS_connect 42
19 #define SYS_clone 56
20 #define SYS_exit 60
21 #define SYS_kill 62
22 #define SYS_fcntl 72
23 #define SYS_getrlimit 97
24 #define SYS_sigaltstack 131
25 #define SYS_arch_prctl 158
26 #define SYS_gettid 186
27 #define SYS_tkill 200
28 #define SYS_futex 202
29 #define SYS_sched_getaffinity 204
30 #define SYS_epoll_create 213
31 #define SYS_exit_group 231
32 #define SYS_epoll_wait 232
33 #define SYS_epoll_ctl 233
34 #define SYS_pselect6 270
35 #define SYS_epoll_create1 291</code></pre>
<p>这些 syscall 理论上都是不会在执行期间被调度器剥离掉 p 的,所以执行成功之后 goroutine 会继续执行,而不像用户的 goroutine 一样,若被剥离 p 会进入等待队列。</p>
<p><strong>▎和调度的交互</strong></p>
<p>既然要和调度交互,那友好地通知我要 syscall 了: entersyscall,我完事了: exitsyscall。</p>
<p>所以这里的交互指的是用户代码使用 syscall 库时和调度器的交互。runtime 里的 syscall 不走这套流程。</p>
<p><strong>▎entersyscall</strong></p>
<pre><code> 1// syscall 库和 cgo 调用的标准入口
2//go:nosplit
3func entersyscall() {
4 reentersyscall(getcallerpc(), getcallersp())
5}
6
7//go:nosplit
8func reentersyscall(pc, sp uintptr) {
9 _g_ := getg()
10
11 // 需要禁止 g 的抢占
12 _g_.m.locks++
13
14 // entersyscall 中不能调用任何会导致栈增长/分裂的函数
15 _g_.stackguard0 = stackPreempt
16 // 设置 throwsplit,在 newstack 中,如果发现 throwsplit 是 true
17 // 会直接 crash
18 // 下面的代码是 newstack 里的
19 // if thisg.m.curg.throwsplit {
20 // throw("runtime: stack split at bad time")
21 // }
22 _g_.throwsplit = true
23
24 // Leave SP around for GC and traceback.
25 // 保存现场,在 syscall 之后会依据这些数据恢复现场
26 save(pc, sp)
27 _g_.syscallsp = sp
28 _g_.syscallpc = pc
29 casgstatus(_g_, _Grunning, _Gsyscall)
30 if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
31 systemstack(func() {
32 print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
33 throw("entersyscall")
34 })
35 }
36
37 if atomic.Load(&sched.sysmonwait) != 0 {
38 systemstack(entersyscall_sysmon)
39 save(pc, sp)
40 }
41
42 if _g_.m.p.ptr().runSafePointFn != 0 {
43 // runSafePointFn may stack split if run on this stack
44 systemstack(runSafePointFn)
45 save(pc, sp)
46 }
47
48 _g_.m.syscalltick = _g_.m.p.ptr().syscalltick
49 _g_.sysblocktraced = true
50 _g_.m.mcache = nil
51 _g_.m.p.ptr().m = 0
52 atomic.Store(&_g_.m.p.ptr().status, _Psyscall)
53 if sched.gcwaiting != 0 {
54 systemstack(entersyscall_gcwait)
55 save(pc, sp)
56 }
57
58 _g_.m.locks--
59}</code></pre>
<p>可以看到,进入 syscall 的 G 是铁定不会被抢占的。</p>
<p><strong>▎exitsyscall</strong></p>
<pre><code>1// g 已经退出了 syscall
2// 需要准备让 g 在 cpu 上重新运行
3// 这个函数只会在 syscall 库中被调用,在 runtime 里用的 low-level syscall
4// 不会用到
5// 不能有 write barrier,因为 P 可能已经被偷走了
6//go:nosplit
7//go:nowritebarrierrec
8func exitsyscall(dummy int32) {
9 _g_ := getg()
10
11 _g_.m.locks++ // see comment in entersyscall
12 if getcallersp(unsafe.Pointer(&dummy)) > _g_.syscallsp {
13 // throw calls print which may try to grow the stack,
14 // but throwsplit == true so the stack can not be grown;
15 // use systemstack to avoid that possible problem.
16 systemstack(func() {
17 throw("exitsyscall: syscall frame is no longer valid")
18 })
19 }
20
21 _g_.waitsince = 0
22 oldp := _g_.m.p.ptr()
23 if exitsyscallfast() {
24 if _g_.m.mcache == nil {
25 systemstack(func() {
26 throw("lost mcache")
27 })
28 }
29 // 目前有 p,可以运行
30 _g_.m.p.ptr().syscalltick++
31 // 把 g 的状态修改回 running
32 casgstatus(_g_, _Gsyscall, _Grunning)
33
34 // 垃圾收集未在运行(因为我们这段逻辑在执行)
35 // 所以清理掉 syscallsp 是安全的
36 _g_.syscallsp = 0
37 _g_.m.locks--
38 if _g_.preempt {
39 // 防止在 newstack 中清理掉 preemption 标记
40 _g_.stackguard0 = stackPreempt
41 } else {
42 // 否则恢复在 entersyscall/entersyscallblock 中破坏掉的正常的 _StackGuard
43 _g_.stackguard0 = _g_.stack.lo + _StackGuard
44 }
45 _g_.throwsplit = false
46 return
47 }
48
49 _g_.sysexitticks = 0
50 _g_.m.locks--
51
52 // 调用 scheduler
53 mcall(exitsyscall0)
54
55 if _g_.m.mcache == nil {
56 systemstack(func() {
57 throw("lost mcache")
58 })
59 }
60
61 // 调度器返回了,所以我们可以清理掉在 syscall 期间为垃圾收集器
62 // 准备的 syscallsp 信息了
63 // 需要一直等待到 gosched 返回,我们不确定垃圾收集器是不是在运行
64 _g_.syscallsp = 0
65 _g_.m.p.ptr().syscalltick++
66 _g_.throwsplit = false
67}</code></pre>
<p>这里还调用了 exitsyscallfast 和 exitsyscall0。</p>
<p><strong>▎exitsyscallfast</strong></p>
<pre><code> 1//go:nosplit
2func exitsyscallfast() bool {
3 _g_ := getg()
4
5 // Freezetheworld sets stopwait but does not retake P's.
6 if sched.stopwait == freezeStopWait {
7 _g_.m.mcache = nil
8 _g_.m.p = 0
9 return false
10 }
11
12 // Try to re-acquire the last P.
13 if _g_.m.p != 0 && _g_.m.p.ptr().status == _Psyscall && atomic.Cas(&_g_.m.p.ptr().status, _Psyscall, _Prunning) {
14 // There's a cpu for us, so we can run.
15 exitsyscallfast_reacquired()
16 return true
17 }
18
19 // Try to get any other idle P.
20 oldp := _g_.m.p.ptr()
21 _g_.m.mcache = nil
22 _g_.m.p = 0
23 if sched.pidle != 0 {
24 var ok bool
25 systemstack(func() {
26 ok = exitsyscallfast_pidle()
27 })
28 if ok {
29 return true
30 }
31 }
32 return false
33}</code></pre>
<p>总之就是努力获取一个 P 来执行 syscall 之后的逻辑。如果哪都没有 P 可以给我们用,那就进入 exitsyscall0 了。</p>
<pre><code>1 mcall(exitsyscall0)</code></pre>
<p>调用 exitsyscall0 时,会切换到 g0 栈。</p>
<p><strong>▎exitsyscall0</strong></p>
<pre><code>
1// 在 exitsyscallfast 中吃瘪了,没办法,慢慢来
2// 把 g 的状态设置成 runnable,先进 runq 等着
3//go:nowritebarrierrec
4func exitsyscall0(gp *g) {
5 _g_ := getg()
6
7 casgstatus(gp, _Gsyscall, _Grunnable)
8 dropg()
9 lock(&sched.lock)
10 _p_ := pidleget()
11 if _p_ == nil {
12 // 如果 P 被人偷跑了
13 globrunqput(gp)
14 } else if atomic.Load(&sched.sysmonwait) != 0 {
15 atomic.Store(&sched.sysmonwait, 0)
16 notewakeup(&sched.sysmonnote)
17 }
18 unlock(&sched.lock)
19 if _p_ != nil {
20 // 如果现在还有 p,那就用这个 p 执行
21 acquirep(_p_)
22 execute(gp, false) // Never returns.
23 }
24 if _g_.m.lockedg != 0 {
25 // 设置了 LockOsThread 的 g 的特殊逻辑
26 stoplockedm()
27 execute(gp, false) // Never returns.
28 }
29 stopm()
30 schedule() // Never returns.
31}
</code></pre>
<p><strong>▎entersyscallblock</strong></p>
<p>知道自己会 block,直接就把 p 交出来了。</p>
<pre><code> 1// 和 entersyscall 一样,就是会直接把 P 给交出去,因为知道自己是会阻塞的
2//go:nosplit
3func entersyscallblock(dummy int32) {
4 _g_ := getg()
5
6 _g_.m.locks++ // see comment in entersyscall
7 _g_.throwsplit = true
8 _g_.stackguard0 = stackPreempt // see comment in entersyscall
9 _g_.m.syscalltick = _g_.m.p.ptr().syscalltick
10 _g_.sysblocktraced = true
11 _g_.m.p.ptr().syscalltick++
12
13 // Leave SP around for GC and traceback.
14 pc := getcallerpc()
15 sp := getcallersp(unsafe.Pointer(&dummy))
16 save(pc, sp)
17 _g_.syscallsp = _g_.sched.sp
18 _g_.syscallpc = _g_.sched.pc
19 if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
20 sp1 := sp
21 sp2 := _g_.sched.sp
22 sp3 := _g_.syscallsp
23 systemstack(func() {
24 print("entersyscallblock inconsistent ", hex(sp1), " ", hex(sp2), " ", hex(sp3), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
25 throw("entersyscallblock")
26 })
27 }
28 casgstatus(_g_, _Grunning, _Gsyscall)
29 if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
30 systemstack(func() {
31 print("entersyscallblock inconsistent ", hex(sp), " ", hex(_g_.sched.sp), " ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
32 throw("entersyscallblock")
33 })
34 }
35
36 // 直接调用 entersyscallblock_handoff 把 p 交出来了
37 systemstack(entersyscallblock_handoff)
38
39 // Resave for traceback during blocked call.
40 save(getcallerpc(), getcallersp(unsafe.Pointer(&dummy)))
41
42 _g_.m.locks--
43}</code></pre>
<p>这个函数只有一个调用方 notesleepg,这里就不再赘述了。</p>
<p><strong>▎entersyscallblock_handoff</strong></p>
<pre><code>1 func entersyscallblock_handoff() {
2 handoffp(releasep())
3 }</code></pre>
<p>比较简单。</p>
<p><strong>▎entersyscall_sysmon</strong></p>
<pre><code>1 func entersyscall_sysmon() {
2 lock(&sched.lock)
3 if atomic.Load(&sched.sysmonwait) != 0 {
4 atomic.Store(&sched.sysmonwait, 0)
5 notewakeup(&sched.sysmonnote)
6 }
7 unlock(&sched.lock)
8 }</code></pre>
<p><strong>▎entersyscall_gcwait</strong></p>
<pre><code> 1 func entersyscall_gcwait() {
2 _g_ := getg()
3 _p_ := _g_.m.p.ptr()
4
5 lock(&sched.lock)
6 if sched.stopwait > 0 && atomic.Cas(&_p_.status, _Psyscall, _Pgcstop) {
7 _p_.syscalltick++
8 if sched.stopwait--; sched.stopwait == 0 {
9 notewakeup(&sched.stopnote)
10 }
11 }
12 unlock(&sched.lock)
13 }</code></pre>
<p><strong>▎总结</strong></p>
<p>提供给用户使用的系统调用,基本都会通知 runtime,以 entersyscall,exitsyscall 的形式来告诉 runtime,在这个 syscall 阻塞的时候,由 runtime 判断是否把 P 腾出来给其它的 M 用。解绑定指的是把 M 和 P 之间解绑,如果绑定被解除,在 syscall 返回时,这个 g 会被放入执行队列 runq 中。</p>
<p>同时 runtime 又保留了自己的特权,在执行自己的逻辑的时候,我的 P 不会被调走,这样保证了在 Go 自己“底层”使用的这些 syscall 返回之后都能被立刻处理。</p>
<p>所以同样是 epollwait,runtime 用的是不能被别人打断的,你用的 syscall.EpollWait 那显然是没有这种特权的。</p>
<p><strong>▎END</strong></p>
<p>参考资料如下<br><a href="https://link.segmentfault.com/?enc=xxwu6PVrcAlgtj93j%2BJtHg%3D%3D.nIdKIalLQkvRAqBuf0xyHHPiFjsS6HJZHmf1EdTF%2BTGZlde2uJKHM3%2FFvB%2BNE2GGTc4O9nUQFpsAIyuqvngY%2Bw%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=bO5A8Q%2Fs8q6aTPCeb2sCDA%3D%3D.wcCrKj8aoIF3IXVAXTqITaggUL8CNqhDkxupyhf1nqg%3D" rel="nofollow">https://z.didi.cn/1HecgP</a></p>
<p><img src="/img/bVbtsKC?w=816&h=443" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
饶全成:汇编角度看 Slice,一个新的世界
https://segmentfault.com/a/1190000019375454
2019-06-03T17:42:58+08:00
2019-06-03T17:42:58+08:00
滴滴技术
https://segmentfault.com/u/didijishu
7
<p><strong>出品 | 滴滴技术</strong><br><strong>作者 | 饶成全</strong></p>
<p><img src="/img/bVbtsjR?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>前言:Go 语言的 slice 很好用,不过也有一些坑。slice 是 Go 语言一个很重要的数据结构。网上已经有很多文章写过了,似乎没必要再写。但是每个人看问题的视角不同,写出来的东西自然也不一样。我这篇会从更底层的汇编语言去解读它。而且在我写这篇文章的过程中,发现绝大部分文章都存在一些问题,文章里会讲到,这里先不展开。</p>
<p>希望以后有人想和你讨论 slice,本篇文章能够对你有所帮助。</p>
<p>▎阅读索引</p>
<p>1.关于 slice<br>2.slice 的创建</p>
<ul>
<li>直接声明</li>
<li>字面量</li>
<li>make</li>
<li>截取</li>
</ul>
<p>3.slice 和数组的区别<br>4.append 到底做了什么<br>5.为什么 nil slice 可以直接 append<br>6.传 slice 和 slice 指针有什么区别<br>7.总结<br>8.参考资料</p>
<p>▎关于 slice</p>
<p>slice 翻译成中文就是切片,它和数组(array)很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。但是它比数组更灵活,可以自动地进行扩容。</p>
<p>了解 slice 的本质,最简单的方法就是看它的源代码:</p>
<pre><code>1 // runtime/slice.go
2 type slice struct {
3 array unsafe.Pointer // 元素指针
4 len int // 长度
5 cap int // 容量
6 }</code></pre>
<p>看到了吗,slice 共有三个属性: 指针,指向底层数组; 长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度; 容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度。</p>
<p><img src="/img/bVbtslD?w=632&h=494" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtsmh?w=754&h=215" alt="图片描述" title="图片描述"></p>
<p>▎直接声明</p>
<p>第一种创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。</p>
<p>这里比较混淆的是 empty slice,它的长度和容量也都为 0 ,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0 。空切片和 nil 比较的结果为 false 。</p>
<p>它们的内部结构如下图:</p>
<p><img src="/img/bVbtsmq?w=968&h=322" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtsmu?w=741&h=207" alt="图片描述" title="图片描述"></p>
<p>nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。</p>
<p>关于nil slice和empty slice的探索可以参考公众号“码洞”作者老钱写的一篇文章《深度解析 Go 语言中「切片」的三种特殊状态》,地址附在了参考资料部分。</p>
<p>▎字面量</p>
<p>比较简单,直接用初始化表达式创建。</p>
<pre><code>
1 package main
2
3 import "fmt"
4
5 func main() {
6 s1 := []int{0, 1, 2, 3, 8: 100}
7 fmt.Println(s1, len(s1), cap(s1))
8 }</code></pre>
<p>运行结果:</p>
<pre><code>1 [0 1 2 3 0 0 0 0 100] 9 9</code></pre>
<p>唯一值得注意的是上面的代码例子中使用了索引号,直接赋值,这样,其他未注明的元素则默认 0 值。</p>
<p>▎make</p>
<p>make函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。</p>
<p>在《走进Go的底层》文章中,我们学到了汇编这个工具,这次我们再次请出汇编来更深入地看看slice。建议先看完上一篇,再继续阅读本文效果更佳。</p>
<p>先来一小段玩具代码,使用 make 关键字创建 slice:</p>
<pre><code>
1 package main
2
3 import "fmt"
4
5 func main() {
6 slice := make([]int, 5, 10) // 长度为5,容量为10
7 slice[2] = 2 // 索引为2的元素赋值为2
8 fmt.Println(slice)
9 }</code></pre>
<p>执行如下命令,得到 Go 汇编代码:</p>
<pre><code>1 go tool compile -S main.go</code></pre>
<p>我们只关注main函数:</p>
<pre><code>
1 0x0000 00000 (main.go:5)TEXT "".main(SB), $96-0
2 0x0000 00000 (main.go:5)MOVQ (TLS), CX
3 0x0009 00009 (main.go:5)CMPQ SP, 16(CX)
4 0x000d 00013 (main.go:5)JLS 228
5 0x0013 00019 (main.go:5)SUBQ $96, SP
6 0x0017 00023 (main.go:5)MOVQ BP, 88(SP)
7 0x001c 00028 (main.go:5)LEAQ 88(SP), BP
8 0x0021 00033 (main.go:5)FUNCDATA $0,
gclocals·69c1753bd5f81501d95132d08af04464(SB)
9 0x0021 00033 (main.go:5)FUNCDATA $1, gclocals·57cc5e9a024203768cbab1c731570886(SB)
10 0x0021 00033 (main.go:5)LEAQ type.int(SB), AX
11 0x0028 00040 (main.go:6)MOVQ AX, (SP)
12 0x002c 00044 (main.go:6)MOVQ $5, 8(SP)
13 0x0035 00053 (main.go:6)MOVQ $10, 16(SP)
14 0x003e 00062 (main.go:6)PCDATA $0, $0
15 0x003e 00062 (main.go:6)CALL runtime.makeslice(SB)
16 0x0043 00067 (main.go:6)MOVQ 24(SP), AX
17 0x0048 00072 (main.go:6)MOVQ 32(SP), CX
18 0x004d 00077 (main.go:6)MOVQ 40(SP), DX
19 0x0052 00082 (main.go:7)CMPQ CX, $2
20 0x0056 00086 (main.go:7)JLS 221
21 0x005c 00092 (main.go:7)MOVQ $2, 16(AX)
22 0x0064 00100 (main.go:8)MOVQ AX, ""..autotmp_2+64(SP)
23 0x0069 00105 (main.go:8)MOVQ CX, ""..autotmp_2+72(SP)
24 0x006e 00110 (main.go:8)MOVQ DX, ""..autotmp_2+80(SP)
25 0x0073 00115 (main.go:8)MOVQ $0, ""..autotmp_1+48(SP)
26 0x007c 00124 (main.go:8)MOVQ $0, ""..autotmp_1+56(SP)
27 0x0085 00133 (main.go:8)LEAQ type.[]int(SB), AX
28 0x008c 00140 (main.go:8)MOVQ AX, (SP)
29 0x0090 00144 (main.go:8)LEAQ ""..autotmp_2+64(SP), AX
30 0x0095 00149 (main.go:8)MOVQ AX, 8(SP)
31 0x009a 00154 (main.go:8)PCDATA $0, $1
32 0x009a 00154 (main.go:8)CALL runtime.convT2Eslice(SB)
33 0x009f 00159 (main.go:8)MOVQ 16(SP), AX
34 0x00a4 00164 (main.go:8)MOVQ 24(SP), CX
35 0x00a9 00169 (main.go:8)MOVQ AX, ""..autotmp_1+48(SP)
36 0x00ae 00174 (main.go:8)MOVQ CX, ""..autotmp_1+56(SP)
37 0x00b3 00179 (main.go:8)LEAQ ""..autotmp_1+48(SP), AX
38 0x00b8 00184 (main.go:8)MOVQ AX, (SP)
39 0x00bc 00188 (main.go:8)MOVQ $1, 8(SP)
40 0x00c5 00197 (main.go:8)MOVQ $1, 16(SP)
41 0x00ce 00206 (main.go:8)PCDATA $0, $1
42 0x00ce 00206 (main.go:8)CALL fmt.Println(SB)
43 0x00d3 00211 (main.go:9)MOVQ 88(SP), BP
44 0x00d8 00216 (main.go:9)ADDQ $96, SP
45 0x00dc 00220 (main.go:9)RET
46 0x00dd 00221 (main.go:7)PCDATA $0, $0
47 0x00dd 00221 (main.go:7)CALL runtime.panicindex(SB)
48 0x00e2 00226 (main.go:7)UNDEF
49 0x00e4 00228 (main.go:7)NOP
50 0x00e4 00228 (main.go:5)PCDATA $0, $-1
51 0x00e4 00228 (main.go:5)CALL runtime.morestack_noctxt(SB)
52 0x00e9 00233 (main.go:5)JMP 0</code></pre>
<p>先说明一下,Go 语言汇编 FUNCDATA 和 PCDATA 是编译器产生的,用于保存一些和垃圾收集相关的信息,我们先不用 care。</p>
<p>以上汇编代码行数比较多,没关系,因为命令都比较简单,而且我们的 Go 源码也足够简单,没有理由看不明白。</p>
<p>我们先从上到下扫一眼,看到几个关键函数:</p>
<pre><code>
1 CALL runtime.makeslice(SB)
2 CALL runtime.convT2Eslice(SB)
3 CALL fmt.Println(SB)
4 CALL runtime.morestack_noctxt(SB)</code></pre>
<p><img src="/img/bVbtspA?w=715&h=184" alt="图片描述" title="图片描述"></p>
<p>1 是创建 slice 相关的;2 是类型转换;调用 fmt.Println 需要将 slice 作一个转换; 3 是打印语句;4是栈空间扩容函数,在函数开始处,会检查当前栈空间是否足够,不够的话需要调用它来进行扩容。暂时可以忽略。</p>
<p>调用了函数就会涉及到参数传递,Go 的参数传递都是通过 栈空间完成的。接下来,我们详细分析这整个过程。<br><img src="/img/bVbtspF?w=717&h=384" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtsp2?w=1080&h=791" alt="图片描述" title="图片描述"></p>
<p>左边是栈上的数据,右边是堆上的数据。array 指向 slice 的底层数据,被分配到堆上了。注意,栈上的地址是从高向低增长;堆则从低向高增长。栈左边的数字表示对应的汇编代码的行数,栈右边箭头则表示栈地址。(48)SP、(56)SP 表示的内容接着往下看。</p>
<p>注意,在图中,栈地址是从下往上增长,所以 SP 表示的是图中 *_type 所在的位置,其它的依此类推。</p>
<p><img src="/img/bVbtsqh?w=712&h=112" alt="图片描述" title="图片描述"></p>
<p>convT2Eslice 的函数声明如下:</p>
<pre><code>1 func convT2Eslice(t *_type, elem unsafe.Pointer) (e eface) </code></pre>
<p>第一个参数是指针 *_type,_type是一个表示类型的结构体,这里传入的就是 slice的类型 []int;第二个参数则是元素的指针,这里传入的就是 slice 底层数组的首地址。</p>
<p>返回值 eface 的结构体定义如下:</p>
<pre><code>1 type eface struct {
2 _type *_type
3 data unsafe.Pointer
4 }</code></pre>
<p>由于我们会调用 fmt.Println(slice),看下函数原型:</p>
<pre><code>1 func Println(a ...interface{}) (n int, err error)</code></pre>
<p>Println 接收 interface 类型,因此我们需要将 slice 转换成 interface 类型。由于 slice 没有方法,是个“空 interface”。因此会调用 convT2Eslice 完成这一转换过程。</p>
<p>convT2Eslice 函数返回的是类型指针和数据地址。源码就不贴了,大体流程是:调用 mallocgc 分配一块内存,把数据 copy 进到新的内存,然后返回这块内存的地址,*_type 则直接返回传入的参数。</p>
<p><img src="/img/bVbtsqz?w=1080&h=609" alt="图片描述" title="图片描述"></p>
<p>32(SP) 和 40(SP) 其实是 makeslice 函数的返回值,这里可以忽略。<br>还剩 fmt.Println(slice) 最后一个函数调用了,我们继续。</p>
<p><img src="/img/bVbtsqQ?w=720&h=112" alt="图片描述" title="图片描述"></p>
<p>最后,我们看下 main 函数栈帧的开始和收尾部分。</p>
<pre><code>
1 0x0013 00019 (main.go:5)SUBQ $96, SP
2 0x0017 00023 (main.go:5)MOVQ BP, 88(SP)
3 0x001c 00028 (main.go:5)LEAQ 88(SP), BP
4 …………………………
5 0x00d3 00211 (main.go:9)MOVQ 88(SP), BP
6 0x00d8 00216 (main.go:9)ADDQ $96, SP
7 RET</code></pre>
<p>BP 可以理解为保存了当前函数栈帧栈底的地址,SP则保存栈顶的地址。<br>初始,BP 和 SP 分别有一个初始状态。</p>
<p>main 函数执行的时候,先根据 main 函数栈帧大小确定 SP 的新指向,使得 main 函数栈帧大小达到 96B。之后把老的 BP 保存到 main 函数栈帧的底部,并使 BP 寄存器重新指向新的栈底,也就是 main 函数栈帧的栈底。</p>
<p>最后,当 main 函数执行完毕,把它栈底的 BP 给回弹回到 BP 寄存器,恢复调用前的初始状态。一切都像是没有发生一样,完美的现场。</p>
<p><img src="/img/bVbtsq7?w=1080&h=500" alt="图片描述" title="图片描述"></p>
<p>这部分,又详细地分析了一遍函数调用的过程。一方面,让大家复习一下上一篇文章讲的内容;另一方面,向大家展示如何找到 Go 中的一个函数背后真实调用了哪些函数。像例子中,我们就看到了 make 函数背后,实际上是调用了 makeslice 函数;还有一点,让大家对汇编不那么“惧怕”,可以轻松地分析一些东西。</p>
<p>▎截取</p>
<p>截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置。</p>
<p>基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。</p>
<p>值得注意的是,新老 slice 或者新 slice 老数组互相影响的前提是两者共用底层数组,如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组。</p>
<p>截取操作采用如下方式:</p>
<pre><code> 1 data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
2 slice := data[2:4:6] // data[low, high, max]</code></pre>
<p>对 data 使用3个索引值,截取出新的 slice 。这里 data 可以是数组或者 slice。low 是最低索引值,这里是闭区间,也就是说第一个元素是 data 位于 low 索引处的元素;而 high 和 max 则是开区间,表示最后一个元素只能是索引 high-1 处的元素,而最大容量则只能是索引 max-1 处的元素。</p>
<pre><code>1 max >= high >= low</code></pre>
<p>当 high == low 时,新 slice 为空。</p>
<p>还有一点,high 和 max 必须在老数组或者老 slice 的容量(cap)范围内。</p>
<p>来看一个例子,来自雨痕大佬《Go学习笔记》第四版,P43页,参考资料里有开源书籍地址。这里我会进行扩展,并会作详细说明:</p>
<pre><code>
1 package main
2
3 import "fmt"
4
5 func main() {
6 slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
7 s1 := slice[2:5]
8 s2 := s1[2:6:7]
9
10 s2 = append(s2, 100)
11 s2 = append(s2, 200)
12
13 s1[2] = 20
14
15 fmt.Println(s1)
16 fmt.Println(s2)
17 fmt.Println(slice)
18 }</code></pre>
<p>先看下代码运行的结果:</p>
<pre><code>
1 [2 3 20]
2 [4 5 6 7 100 200]
3 [0 1 2 3 20 5 6 7 100 9]</code></pre>
<p>我们来走一遍代码,初始状态如下:</p>
<pre><code>1 slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
2 s1 := slice[2:5]
3 s2 := s1[2:6:7]</code></pre>
<p>s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。</p>
<p><img src="/img/bVbtssG?w=1080&h=429" alt="图片描述" title="图片描述"></p>
<p>接着,向 s2 尾部追加一个元素 100:</p>
<pre><code>1 s2 = append(s2, 100)</code></pre>
<p>s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。</p>
<p><img src="/img/bVbtssV?w=1080&h=429" alt="图片描述" title="图片描述"></p>
<p>再次向 s2 追加元素200:</p>
<pre><code>
1 s2 = append(s2, 100)</code></pre>
<p>这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。</p>
<p><img src="/img/bVbtss8?w=1080&h=438" alt="图片描述" title="图片描述"></p>
<p>最后,修改 s1 索引为2位置的元素:</p>
<pre><code>1 s1[2] = 20</code></pre>
<p>这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。</p>
<p><img src="/img/bVbtsuy?w=1080&h=438" alt="图片描述" title="图片描述"></p>
<p>再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。</p>
<p>至于,我们想在汇编层面看看到底它们是如何共享底层数组的,限于篇幅,这里不再展开。感兴趣的同学可以在公众号后台回复:切片截取。</p>
<p>我会给你详细分析函数调用关系,对共享底层数组的行为也会一目了然。</p>
<p>▎slice 和数组的区别在哪</p>
<p>slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。</p>
<p>数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。</p>
<p>而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。</p>
<p>▎append 到底做了什么</p>
<p>先来看看 append 函数的原型:</p>
<pre><code>1 func append(slice []Type, elems ...Type) []Type</code></pre>
<p>append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。</p>
<pre><code>
1 slice = append(slice, elem1, elem2)
2 slice = append(slice, anotherSlice...)</code></pre>
<p>append函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。</p>
<pre><code>1 append(slice, elem1, elem2)
2 append(slice, anotherSlice...)</code></pre>
<p>所以上面的用法是错的,不能编译通过。</p>
<p>使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。</p>
<p>这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。</p>
<p>新 slice 预留的 buffer 大小是有一定规律的。网上大多数的文章都是这样描述的:</p>
<p><em>当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。</em></p>
<p>我在这里先说结论:以上描述是错误的。</p>
<p>为了说明上面的规律是错误的,我写了一小段玩具代码:</p>
<pre><code>1 package main
2
3 import "fmt"
4
5 func main() {
6 s := make([]int, 0)
7
8 oldCap := cap(s)
9
10 for i := 0; i < 2048; i++ {
11 s = append(s, i)
12
13 newCap := cap(s)
14
15 if newCap != oldCap {
16 fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap)
17 oldCap = newCap
18 }
19 }
20 }</code></pre>
<p>我先创建了一个空的 slice,然后,在一个循环里不断往里面 append 新的元素。然后记录容量的变化,并且每当容量发生变化的时候,记录下老的容量,以及添加完元素之后的容量,同时记下此时 slice 里的元素。这样,我就可以观察,新老 slice 的容量变化情况,从而找出规律。</p>
<p>运行结果:</p>
<pre><code>
1 [0 -> -1] cap = 0 | after append 0 cap = 1
2 [0 -> 0] cap = 1 | after append 1 cap = 2
3 [0 -> 1] cap = 2 | after append 2 cap = 4
4 [0 -> 3] cap = 4 | after append 4 cap = 8
5 [0 -> 7] cap = 8 | after append 8 cap = 16
6 [0 -> 15] cap = 16 | after append 16 cap = 32
7 [0 -> 31] cap = 32 | after append 32 cap = 64
8 [0 -> 63] cap = 64 | after append 64 cap = 128
9 [0 -> 127] cap = 128 | after append 128 cap = 256
10 [0 -> 255] cap = 256 | after append 256 cap = 512
11 [0 -> 511] cap = 512 | after append 512 cap = 1024
12 [0 -> 1023] cap = 1024 | after append 1024 cap = 1280
13 [0 -> 1279] cap = 1280 | after append 1280 cap = 1696
14 [0 -> 1695] cap = 1696 | after append 1696 cap = 2304</code></pre>
<p>在老 slice 容量小于1024的时候,新 slice 的容量的确是老 slice 的2倍。目前还算正确。</p>
<p>但是,当老 slice 容量大于等于 1024 的时候,情况就有变化了。当向 slice 中添加元素 1280 的时候,老 slice 的容量为 1280,之后变成了 1696,两者并不是 1.25 倍的关系(1696/1280=1.325)。添加完 1696 后,新的容量 2304 当然也不是 1696 的 1.25 倍。</p>
<p>可见,现在网上各种文章中的扩容策略并不正确。我们直接搬出源码:源码面前,了无秘密。</p>
<p>从前面汇编代码我们也看到了,向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,所以我们直接看它的代码。</p>
<pre><code>1 // go 1.9.5 src/runtime/slice.go:82
2 func growslice(et *_type, old slice, cap int) slice {
3 // ……
4 newcap := old.cap
5 doublecap := newcap + newcap
6 if cap > doublecap {
7 newcap = cap
8 } else {
9 if old.len < 1024 {
10 newcap = doublecap
11 } else {
12 for newcap < cap {
13 newcap += newcap / 4
14 }
15 }
16 }
17 // ……
18
19 capmem = roundupsize(uintptr(newcap) * ptrSize)
20 newcap = int(capmem / ptrSize)
21 }</code></pre>
<p>看到了吗?如果只看前半部分,现在网上各种文章里说的 newcap 的规律是对的。现实是,后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。</p>
<p>之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。</p>
<p>最后,向 growslice 函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。</p>
<p>关于 append,我们最后来看一个例子,来源于参考资料部分的【Golang Slice的扩容规则】。</p>
<pre><code>1 package main
2
3 import "fmt"
4
5 func main() {
6 s := []int{1,2}
7 s = append(s,4,5,6)
8 fmt.Printf("len=%d, cap=%d",len(s),cap(s))
9 }
</code></pre>
<p>运行结果是:</p>
<pre><code>1 len=5, cap=6</code></pre>
<p>如果按网上各种文章中总结的那样:小于原 slice 长度小于 1024 的时候,容量每次增加 1 倍。添加元素 4 的时候,容量变为4;添加元素 5 的时候不变;添加元素 6 的时候容量增加 1 倍,变成 8。</p>
<p>那上面代码的运行结果就是:</p>
<pre><code>1 len=5, cap=8</code></pre>
<p>这是错误的!我们来仔细看看,为什么会这样,再次搬出代码:</p>
<pre><code>
1 // go 1.9.5 src/runtime/slice.go:82
2 func growslice(et *_type, old slice, cap int) slice {
3 // ……
4 newcap := old.cap
5 doublecap := newcap + newcap
6 if cap > doublecap {
7 newcap = cap
8 } else {
9 // ……
10 }
11 // ……
12
13 capmem = roundupsize(uintptr(newcap) * ptrSize)
14 newcap = int(capmem / ptrSize)
15 }</code></pre>
<p>这个函数的参数依次是 元素的类型,老的 slice,新 slice 最小求的容量。</p>
<p>例子中 s 原来只有 2 个元素,len 和 cap 都为 2,append 了三个元素后,长度变为 3,容量最小要变成 5,即调用 growslice 函数时,传入的第三个参数应该为 5。即 cap=5。而一方面,doublecap 是原 slice容量的 2 倍,等于 4。满足第一个 if 条件,所以 newcap 变成了 5。</p>
<p>接着调用了 roundupsize 函数,传入 40。(代码中ptrSize是指一个指针的大小,在64位机上是8)</p>
<p>我们再看内存对齐,搬出 roundupsize 函数的代码:</p>
<pre><code>1 // src/runtime/msize.go:13func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) } else { //…… } } //……}const _MaxSmallSize = 32768const smallSizeMax = 1024const smallSizeDiv = 8
</code></pre>
<p>很明显,我们最终将返回这个式子的结果:</p>
<pre><code>1 class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]</code></pre>
<p>这是 Go 源码中有关内存分配的两个 slice。class_to_size通过 spanClass获取 span划分的 object大小。而 size_to_class8 表示通过 size 获取它的 spanClass。</p>
<pre><code>
1 var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}
2 var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
</code></pre>
<p>我们传进去的 size 等于 40。所以 (size+smallSizeDiv-1)/smallSizeDiv = 5;获取 size_to_class8 数组中索引为 5 的元素为 4;获取 class_to_size 中索引为 4 的元素为 48。</p>
<p>最终,新的 slice 的容量为 6:</p>
<pre><code>1 newcap = int(capmem / ptrSize) // 6</code></pre>
<p>至于,上面的两个魔法数组的由来,暂时就不展开了。</p>
<p>▎为什么 nil slice 可以直接 append</p>
<p>其实 nil slice 或者 empty slice 都是可以通过调用 append 函数来获得底层数组的扩容。最终都是调用 mallocgc 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的nil slice 或 empty slice,然后摇身一变,成为“真正”的 slice 了。</p>
<p>▎传 slice 和 slice 指针有什么区别</p>
<p>前面我们说到,slice 其实是一个结构体,包含了三个成员:len, cap, array。分别表示切片长度,容量,底层数据的地址。</p>
<p>当 slice 作为函数参数时,就是一个普通的结构体。其实很好理解:若直接传 slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。</p>
<p>值的注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。为什么能改变底层数组的数据?很好理解:底层数据在 slice 结构体里是一个指针,仅管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。 但是通过指向底层数据的指针,可以改变切片的底层数据,没有问题。</p>
<p>通过 slice 的 array 字段就可以拿到数组的地址。在代码里,是直接通过类似 s[i]=10 这种操作改变 slice 底层数组元素值。</p>
<p>另外,啰嗦一句,Go 语言的函数参数传递,只有值传递,没有引用传递。后面会再写一篇相关的文章,敬请期待。</p>
<p>再来看一个年幼无知的代码片段:</p>
<pre><code>1 package main
2
3 func main() {
4 s := []int{1, 1, 1}
5 f(s)
6 fmt.Println(s)
7 }
8
9 func f(s []int) {
10 // i只是一个副本,不能改变s中元素的值
11 /*for _, i := range s {
12 i++
13 }
14 */
15
16 for i := range s {
17 s[i] += 1
18 }
19 }</code></pre>
<p>运行一下,程序输出:</p>
<pre><code>1 [2 2 2]</code></pre>
<p>果真改变了原始 slice 的底层数据。这里传递的是一个 slice 的副本,在 f 函数中,s 只是 main 函数中 s 的一个拷贝。在f 函数内部,对 s 的作用并不会改变外层 main 函数的 s。</p>
<p>要想真的改变外层 slice,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。我们再来看一个例子:</p>
<pre><code>1 package main
2
3 import "fmt"
4
5 func myAppend(s []int) []int {
6 // 这里 s 虽然改变了,但并不会影响外层函数的 s
7 s = append(s, 100)
8 return s
9 }
10
11 func myAppendPtr(s *[]int) {
12 // 会改变外层 s 本身
13 *s = append(*s, 100)
14 return
15 }
16
17 func main() {
18 s := []int{1, 1, 1}
19 newS := myAppend(s)
20
21 fmt.Println(s)
22 fmt.Println(newS)
23
24 s = newS
25
26 myAppendPtr(&s)
27 fmt.Println(s)
28 }</code></pre>
<p>运行结果:</p>
<pre><code>1 [1 1 1]
2 [1 1 1 100]
3 [1 1 1 100 100]</code></pre>
<p>myAppend 函数里,虽然改变了 s,但它只是一个值传递,并不会影响外层的 s,因此第一行打印出来的结果仍然是 [1 1 1]。</p>
<p>而 newS 是一个新的 slice,它是基于 s 得到的。因此它打印的是追加了一个 100 之后的结果: [1 1 1 100]。</p>
<p>最后,将 newS 赋值给了 s,s 这时才真正变成了一个新的slice。之后,再给 myAppendPtr 函数传入一个 s 指针,这回它真的被改变了:[1 1 1 100 100]。</p>
<p>▎总结</p>
<p>到此,关于 slice 的部分就讲完了,不知大家有没有看过瘾。我们最后来总结一下:</p>
<ul>
<li>切片是对底层数组的一个抽象,描述了它的一个片段。</li>
<li>切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。</li>
<li>多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。</li>
<li>append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。</li>
<li>扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。</li>
<li>当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。</li>
</ul>
<p>▎END</p>
<p>参考资料如下<br><a href="https://link.segmentfault.com/?enc=yBly23GH9FONrnAj%2FAc3eg%3D%3D.rdNbk7yHlEMZyYhWTdMKgvEs2Ztdkpc6iLe%2BgxLyUqEACJzduze6HHJstNRaid6TKozzJ%2F27zgIoKqECSgexkg%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=R7DI2RHQy2Ot13973CVgaA%3D%3D.3LZy0YGUj7nb%2BrFNI5P60%2Fqz9XrkCwiKixrglobphUo%3D" rel="nofollow">https://z.didi.cn/1HGeUj</a></p>
<p><img src="/img/bVbtdCB?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtsBe?w=810&h=244" alt="图片描述" title="图片描述"></p>
<p>毕业于中科院计算所。17年加入滴滴引擎技术部,负责供需系统的后端研发。Go语言爱好者,热衷于探究技术背后的原理。<br><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
滴滴开源 DroidAssist : 轻量级 Android 字节码编辑插件
https://segmentfault.com/a/1190000019374170
2019-06-03T16:22:25+08:00
2019-06-03T16:22:25+08:00
滴滴技术
https://segmentfault.com/u/didijishu
1
<p>出品 | 滴滴技术<br>作者 | 江义旺</p>
<p><img src="/img/bVbtseG?w=2350&h=1000" alt="图片描述" title="图片描述"></p>
<p>前言:近日,滴滴发布的开源项目 DroidAssist ,提供了一种简单易用、无侵入、配置化、轻量级的 Java 字节码操作方式,只需要在 XML 配置中添加简单的 Java 代码即可实现编译期对 Class 文件的动态修改。</p>
<p>DroidAssist 和其他 AOP 方案不同,它提供了一种简单易用、无侵入、配置化、轻量级的 Java 字节码操作方式,你不需要 Java 字节码的相关知识,只需要在 XML 配置中添加简单的 Java 代码即可实现编译期对 class 文件的动态修改,同时不需要引入其他额外的依赖。</p>
<p>▍起源</p>
<p>作为大型 APP 的代表,滴滴出行乘客端集成了较多的业务线,包含了大量的依赖库,每个版本都有多个团队向乘客端集成大量的代码,而且这些代码都是难以直接追溯到源码的,同时乘客端还有用户量大,日活高,迭代快等特点,这些情况对乘客端的开发和维护形成很大的挑战,主要体现在:问题防范难度大、问题规模大、后期维护成本高。</p>
<p>2018年5月,乘客端团队进行卡顿专项优化, 其中有个问题是:由于安卓系统 SharedPreferences自身机制,当频繁调用 SharedPreferences.apply() 方法时,可能会出现由 QueuedWork.waitToFinish() 造成的卡顿和 ANR。主要原因是系统在 Activity 的 onPause、onStop,以及 Service 的 start 和 stop 生命周期时会执行阻塞等待 QueuedWork 清空,推测系统是为了保证持久化成功率,从而确保用户离开组件之前完成 SharedPreferences 的文件写入。</p>
<p>分析原因之后,我们认为,乘客端 APP 相对处于单一的进程环境,去掉这个持久化阻塞也是可以的。为了解决这个问题,我们决定对系统的 SharedPreferences 进行改造,实现我们自己的 SharedPreferences。</p>
<p>但是随之而来的问题是,我们自定义的 SharedPreferences 怎么以最小的成本接入到乘客端呢?很容易想到以下两种方案:</p>
<ul>
<li>修改所有调用 Context.getSharedPreferences() 的代码,返回我们自己的 SharedPreferences<br> 对象,缺点:改动太多,工作量太大,修改、还原成本太高。</li>
<li>所有的 Application、Activity、Service 类都从统一的的 Base 基类派生,在基类中重写<br> getSharedPreferences 方法返回自定义 SharedPreferences<br> 对象,和方法一相比,此方法代码改动较小,但是也存在是无法修改第三方库,而且工作量也比较大,修改、还原成本也很高的问题。</li>
</ul>
<p>以上两种方式都具有较大的侵入性,会涉及到大量的源码以及依赖库的代码改动,后期维护和升级成本也比较高,为了寻找更加理想的解决方案,我们希望找到一种无侵入的 Mock 工具,能做到不修改代码就能 Mock 所有 getSharedPreferences()方法的调用返回结果,初步有如下两种实现思路:</p>
<ul>
<li>Hook:Hook 技术需要一直处理各种厂商和机型的兼容性问题,有较大的稳定性风险。</li>
<li>AOP:AOP 类框架在编译期实现字节码操作,比较成熟稳定,可以考虑采用,但是经过分析发现,现有的 AOP 框架包括 AspectJ<br> 并不能实现我们需要的 Mock 功能。</li>
</ul>
<p>类似 SharedPreferences 替换这样的需求还有很多,于是我们决定自己开发一个Android 平台 Mock 工具,经过调研之后,我们确定了字节码修改的技术方向,通过修改字节码实现这样的需求,由此 DroidAssist 应运而生。</p>
<p>项目地址:<a href="https://link.segmentfault.com/?enc=ngpJ9MC7CoePFiEryV%2BniQ%3D%3D.1ox6247wx6oSyw9er%2Fsue3fqvrPpVrAxOfApKg%2BxjlsbmDYkTH1C1sHlzgo%2FUcEr" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=%2FeTz7ut6anV24E1vl9Pp%2Bw%3D%3D.Tb5zqLmZ5LDqztsTdo55A2g3VDxTD0WInS7%2BOA%2F8dEEDqs35KKZoKFw5CSP5Uk0w" rel="nofollow">https://github.com/didi/Droid...</a></p>
<p>▍示例</p>
<p>下面例子是背景中提到的 SharedPreferences 改造,添加如下 DroidAssist 配置,在项目编译后,所有调用Context.getSharedPreferences() 的代码,将全部会被修改为返回自定义的 SharedPreferences 实例的代码:</p>
<pre><code>1 <Replace>
2 <MethodCall>
3 DroidAssist
4 <Source>android.content.SharedPreferences android.content.Context.getS
5 haredPreferences(java.lang.String,int)</Source>
6 <Target>{$_= com.didi.quicksilver.QuicksilverPreferencesHelper.getShar
7 edPreferences($0,$$);}</Target>
8 </MethodCall>
9 </Replace></code></pre>
<p>处理前的 class:</p>
<pre><code>1 public class MainActivity extends Activity {
2 @Override
3 protected void onCreate(Bundle savedInstanceState) {
4 super.onCreate(savedInstanceState);
5 SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
6 } }</code></pre>
<p>处理后的 class:</p>
<pre><code>1 public class MainActivity extends Activity {
2 protected void onCreate(Bundle savedInstanceState) {
3 super.onCreate(savedInstanceState);
4 SharedPreferences sp = PreferencesHelper.getSharedPreferences(this
5 , "test", MODE_PRIVATE); // The target method return custom SharedPreferen
6 ces.
7 } }</code></pre>
<p>具体的使用方式及原理可参见 <a href="https://link.segmentfault.com/?enc=LvzJN0zh6Lu1aZ8Ch2Sdrg%3D%3D.b1%2Fh68YSSjEnyVDypbC%2BgiAQtqRFrI1qbEktHDHxuorPLqHkOJj3y%2BqN8wO4zs4g" rel="nofollow">DroidAssist WIKI</a> 。</p>
<p>▍特性</p>
<p>经过不断的打磨完善,DroidAssist 已经从最开始的 Mock 工具扩展成为具有完整 AOP 框架功能的工具,有如下特性。</p>
<p>▍简单易用</p>
<p>采用灵活的配置化方式,使用者只需要依赖一个插件,然后在配置文件中定义字节码处理方式,DroidAssist 就可以根据配置文件处理项目中所有的 class 文件。处理过程以及处理后的代码中都不需要添加额外的依赖,并且不会修改原始代码行号。</p>
<p>▍丰富的字节码处理功能</p>
<p>除了解决我们最初遇到的代码替换问题外,还扩展了其他的 AOP 功能,目前有 4 类 28 种代码修改方式。</p>
<p><img src="/img/bVbtsfW?w=1063&h=635" alt="图片描述" title="图片描述"></p>
<ul>
<li>替换:把指定位置代码替换为指定代码</li>
<li>插入:在指定位置的前后插入指定代码</li>
<li>环绕:在指定位置环绕插入指定代码</li>
<li>增强:TryCatch 对指定代码添加 try catch 代码 Timing 对指定代码添加耗时统计代码</li>
</ul>
<p>▍简单易用</p>
<p>支持增量构建,处理速度快,只占用很少的构建时间。</p>
<p>▍Q&A</p>
<ol><li>DroidAssist 可以实现什么功能?</li></ol>
<p>DroidAssist 可以轻易实现诸如代码替换,代码插入等功能,滴滴出行 APP 利用 DroidAssist 实现了日志输出替换,系统 SharedPreferences 替换,SharedPreferences commit 替换为 apply,Dialog 展示保护,getDeviceId 接口替换,getPackageInfo 接口替换,getSystemService 接口替换,startActivity 保护,匿名线程重命名,线程池创建监控,主线程卡顿监控,文件夹创建监控,Activity 生命周期耗时统计,APP启动耗时统计等功能。</p>
<ol><li>DroidAssist 和 AspectJ 有什么区别?</li></ol>
<p>DroidAssist 采用配置化方案,编写相关配置就可以实现 AOP 的功能,可以完全不用修改 Java 代码;DroidAssist 在使用上使用比较简单,不需要复杂的注解配置;DroidAssist 可以比较方便的实现 AspectJ 不容易实现的代码替换功能。一般情况下使用 DroidAssist 可以完成大部分功能,较复杂情况可以和 AspectJ 配合使用。<br><img src="/img/bVbtrhv?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p>有关安装、使用过程以及常见问题解答,请查看以下链接:<br>GitHub:<a href="https://link.segmentfault.com/?enc=wVDQxx86DKCifaTHCygycw%3D%3D.DPI%2Fwz4%2BEYIzuibuqIbkqAGe%2BsIc3Q4nfIZrWiSndTfCe6gfEvG0K1P3R6UNAzqi" rel="nofollow">https://github.com/didi/Droid...</a><br>Wiki:<a href="https://link.segmentfault.com/?enc=m4HYtfQyqpQnmeenN4I1jw%3D%3D.C%2F6L3UDaIfOs9rixII52fJQC3t37204xhZ%2Fvyig4IrfiVxJa6xBZWBi8GZeIV14q" rel="nofollow">https://github.com/didi/Droid...</a></p>
<p><img src="/img/bVbtrhL?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtsgI?w=2217&h=1200" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
张伦:巧用 webpack loader 实现项目的定制化
https://segmentfault.com/a/1190000019373516
2019-06-03T15:44:52+08:00
2019-06-03T15:44:52+08:00
滴滴技术
https://segmentfault.com/u/didijishu
10
<p>出品 | 滴滴技术<br>作者 | 张伦<br><img src="/img/bVbtr2X?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>前言:随着前端技术的发展,Web 应用变得复杂。为解决开发的复杂度,前端开发也有了模块化的概念。使用 Webpack 完成 模块化的打包构建的方案,可谓尽人皆知。但是利用 Webpack 能做的事情远不止如此。这篇文章从一个独特的角度,利用 Webpack 的特点实现了定制化需求,希望能够对大家有一些启发。</p>
<p>▍背景</p>
<p>有这样的需求:项目交付的给客户时,需要支持针对客户定制产品的 LOGO、登录界面的背景。</p>
<p>▍简单分析</p>
<p>手动替换图片文件再编译的方法肯定是无法接受的。</p>
<p>如果你说采用分支的方式来实现这种需求,我觉得也是不太现实。毕竟,这并不是分支的使用场景。</p>
<p>项目在交付时需要避免交付的代码中包含其他客户的资源和信息。这意味着,通过配置文件等在运行时加载的方式是行不通。</p>
<p>想来想去,问题的本质其实是解决项目编译输出时 CSS 可以使用我们指定的图片文件,而我们需要将这个过程自动化。</p>
<p>▍第一种方案</p>
<p>先来一种简单而又直接的方案:直接替换。其步骤如下:</p>
<ul>
<li>将图片资源放入指定的目录中,按项目 ( 客户 ) 区分。</li>
<li>执行替换图片资源的脚本,使用指定的资源替换。</li>
<li>执行项目的编译命令。</li>
</ul>
<pre><code> 1 // pre-packaging.js
2
3 const path = require("path");
4 const fs = require("fs");
5 const project = process.argv[2];
6 const distPath = path.resolve("./src/static/images"); // 源代码目录
7 const resourcePath = path.resolve("./resources", project); // 项目静态文件目录
8
9 function copyDir(src, dist) {
10 try {
11 fs.accessSync(dist, fs.constants.R_OK | fs.constants.W_OK);
12 } catch (err) {
13 fs.mkdirSync(dist);
14 }
15
16 const copyFile = (src, dist) => {
17 fs.createReadStream(src).pipe(fs.createWriteStream(dist));
18 };
19
20 const dirList = fs.readdirSync(src);
21
22 dirList.forEach(item => {
23 const currentPath = path.resolve(src, item);
24 const currentDistPath = path.resolve(dist, item);
25
26 if (fs.statSync(currentPath).isDirectory()) {
27 copyDir(currentPath, currentDistPath);
28 } else {
29 const src = currentPath;
30 const dist = currentDistPath;
31
32 copyFile(src, dist);
33 }
34 });
35 }
36
37 copyDir(resourcePath, distPath);</code></pre>
<p>执行脚本</p>
<pre><code>1 node ./pre-packaging.js projectname</code></pre>
<p>看起来我们的问题已经得到解决。但是你仔细想想,便会发现,这种方案存在多个不足之处:</p>
<ul>
<li>侵入性强。每次自定义版本构建之后都修改目录中的图片资源,这些修改很容易被同步到远端。</li>
<li>拓展性差。自定义的图片资源必须严格按照源码中的约定,比如图片格式,图片尺寸。每一张图片都需要在代码中提供相应的插槽。</li>
<li>功能单一。只能修改图片的引用,当其他的样式需要调整时便无能为力。</li>
<li>体验性差。将构建过程拆分为准备静态资源和编译两个过程。</li>
</ul>
<p>▍第二种方案</p>
<p>是否有更好的方案?此时我们回到问题:如何实现同一个项目针对不同客户定制界面的Logo和登录背景?</p>
<p>我们需要修改的是什么?CSS!</p>
<p>既想修改 CSS 样式,又想不对源码进行修改,那只有采用 CSS 样式具有的覆盖规则来实现。源文件中设置默认样式,约定使用的 CSS 选择器,通过编译将新的样式文件和源文件合并,所有的样式打包输出。</p>
<p>这种方式有诸多好处:</p>
<ul>
<li>侵入性弱。只需要在项目仓库中维护对应的资源,不影响源代码,交付时也不会包含多余的资源。</li>
<li>拓展性强。自定义的图片资源不在依赖源码,可以使用任意的图片格式。</li>
<li>功能丰富。可以额外增加自定义样式,不限于需求中的 Logo 和背景。</li>
<li>体验好。在编译阶段加载指定的样式,一步到位。</li>
</ul>
<p>说到前端的编译打包,自然想到 Webpack。可以从 Webpack Loader 入手,实现上述过程。</p>
<p>▍Webpack Loader</p>
<p>在 Webpack 的生态中,Loader 用于对模块的源代码进行转换。Loader 可以使你在 import 或"加载"模块时预处理文件。因此,Loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。Loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。</p>
<p>Webpack Loader 的编写可参考<a href="https://link.segmentfault.com/?enc=SMpnACoVitkfyOxXJwGr2g%3D%3D.s%2BySSZv%2B0ltOi3dzFtSR%2BlEHvik%2FANxh%2BQHGPM2ttQ%2FkxC8mLfLW45EcGuswjtjsTL2grhtPDNBT8nf%2BZSq15g%3D%3D" rel="nofollow">官方文档</a>,有非常详细的说明。</p>
<p>以常见的一段 Webpack 配置为例:</p>
<pre><code> 1 module.exports = {
2 entry: [...],
3 output: {...},
4 module: {
5 rules: [
6 ...,
7 {
8 test: /\.less$/,
9 use: [
10 {
11 loader: 'style-loader',
12 },
13 {
14 loader: 'css-loader',
15 },
16 {
17 loader: 'less-loader',
18 }
19 ];
20 }
21 ...,
22 ],
23 },
24 };</code></pre>
<p>上述配置在执行过程中,less文件的编译会按照如下顺序 (<a href="https://link.segmentfault.com/?enc=AhWZib5Q1JsK%2BbssowMoQw%3D%3D.maI%2B13e6k3ZSfNzAjiQif%2BOVN3poZ0lMr%2FmzkQx2g%2B3CdwbJsLJcK8ujzj8na6qz%2BPORd%2FB13twjq%2B8xv4Z1C8A9cyz4b6CI3opz4mLr2Vg9npJBz%2FG%2Fgarjk%2BIucX32" rel="nofollow">Webpack Loader 执行顺序</a>):</p>
<p><img src="/img/bVbtr35?w=1080&h=123" alt="图片描述" title="图片描述"></p>
<p>在整个编译过程中,我们可以在每一个Loader的开始前和结束后合并我们自定义样式,如下图所示:</p>
<p><img src="/img/bVbtr4q?w=1080&h=251" alt="图片描述" title="图片描述"></p>
<p>在less-loader之前加入自定义的CSS样式是最好的时机,为什么呢?有两点:</p>
<ul>
<li>同时支持 CSS 和 Less 两种文件。</li>
<li>在整个编译开始之前加入,对编译的整个过程没有影响。新增的样式同样享受完整编译过程。</li>
</ul>
<p>编译过程修改为如下图所示:</p>
<p><img src="/img/bVbtr4v?w=1080&h=99" alt="图片描述" title="图片描述"></p>
<p>▍开发一个 merge-loader</p>
<p>在目前的场景中,merge-loader 只需要一个参数:自定义样式的文件路径。所以 Webpack 配置文件可以修改为:</p>
<pre><code> 1 const { getOptions } = require('loader-utils');
2
3 module.exports = function (source) {
4 const options = getOptions(this);
5 const { style } = options;
6
7 // 读取样式文件,返回字符串
8 const string = fs.readFileSync(style);
9
10 // 合并到原始文件,返回给下一个loader
11 source += string;
12
13 return source;
14 };</code></pre>
<p>你以为这样就结束了?不,上述逻辑有两个问题还需优化:</p>
<ul>
<li>当样式中存在图片的引用时,以字符串形式拼接在源码样式中会遇到图片路径错误的问题。</li>
<li>只要文件通过了规则/.less&/的匹配,就会执行一次合并的操作。含有<style lang="less"></style><br> 的vue文件也会触发这个规则(虽然重复引用不会增加代码量)。</li>
</ul>
<p>这两个问题的解法如下:</p>
<ul>
<li>使用 @import "path/of/style" 方式合并样式文件。其他的处理交给后面的Loader,保证文件和图片路径引用正确。</li>
<li>增加一个参数target,指定一个文件作为 merge 的对象。</li>
</ul>
<p>这样一来,merge-loader 的逻辑修改如下:</p>
<pre><code> 1 module.exports = {
2 entry: [...],
3 output: {...},
4 module: {
5 rules: [
6 ...,
7 {
8 test: /\.less$/,
9 use: [
10 {
11 loader: 'style-loader',
12 },
13 {
14 loader: 'css-loader',
15 },
16 {
17 loader: 'less-loader',
18 },
19 {
20 loader: path.resolve(__dirname, './loader/merge-less.js'), // 自定义loader文件的路径
21 options: {
22 style: path.resolve(root, 'client/statics/projects/it/style.less'),
23 },
24 }
25 ];
26 }
27 ...,
28 ],
29 },
30 };</code></pre>
<p>▍优化 Loader</p>
<p>最后利用 <a href="https://link.segmentfault.com/?enc=29YzRdUY3kDfTDtdzOS%2Flw%3D%3D.VokZTBGZj%2Bh1yuaRc9bwBNKehQR0P6jfoxTGh3MEY3wKPfEOX65Q8vApeTHmZZ9vlSPs%2Fiu0zVxErlsXbBDlBJi%2FxHE9ysBOFatdGhJbL3InxrhF7MZ4d83HAy84IEbZicYiygER8IeXSg94W%2B6Acg%3D%3D" rel="nofollow">Loader 工具库</a> 来优化代码</p>
<pre><code> 1 const fs = require('fs');
2 const path = require('path');
3 const loaderUtils = require('loader-utils');
4 const validateOptions = require('schema-utils');
5
6 const schema = {
7 type: 'object',
8 properties: {
9 style: {
10 type: 'string',
11 },
12 target: {
13 type: 'string',
14 },
15 },
16 required: [ 'style', 'target' ],
17 };
18
19
20 module.exports = function (source, meta) {
21 const options = loaderUtils.getOptions(this);
22
23 // 验证 options 参数
24 validateOptions(schema, options, 'Loader options');
25
26 let { style, target } = options;
27
28 /*
29 * Loader 原则之一:不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化
30 * 使用 stringifyReques 将绝对路径转换成相对路径
31 */
32 style = loaderUtils.stringifyRequest(this, style);
33
34 if (meta) {
35 const { file, sourceRoot } = meta;
36
37 if (target === path.join(sourceRoot, file)) {
38 const string = `\n @import ${style};\n`;
39
40 source += string;
41 }
42 }
43
44 return source;
45 }</code></pre>
<p>▍结束</p>
<p>借助 Webpack Loader,已经完成了项目的定制化。这种方案的几个特点:</p>
<ul>
<li>侵入性弱。只需要在项目仓库中维护对应的资源,不影响源代码,交付时也不会包含多余的资源。</li>
<li>拓展性强。自定义的图片资源不在依赖源码,可以使用任意的图片格式。</li>
<li>功能丰富。可以额外增加自定义样式,不限于需求中的 Logo 和背景。</li>
<li>体验好。在编译阶段加载指定的样式,一步到位。</li>
</ul>
<p>▍END</p>
<p><img src="/img/bVbtdCB?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtr5U?w=777&h=274" alt="图片描述" title="图片描述"></p>
<p>2015年正式开始职业生涯,2017年加入滴滴。酷爱编程,伪全周期工程师。点子王,爱折腾,喜欢用技术解决问题。梦想做一棵大树,静看时间流逝。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
江义旺:滴滴出行安卓端 finalize time out 的解决方案
https://segmentfault.com/a/1190000019373275
2019-06-03T15:24:46+08:00
2019-06-03T15:24:46+08:00
滴滴技术
https://segmentfault.com/u/didijishu
6
<p><strong>出品 | 滴滴技术</strong><br><strong>作者 | 江义旺</strong></p>
<p><img src="/img/bVbtrZf?w=1080&h=459" alt="图片描述" title="图片描述"></p>
<p>前言:随着安卓 APP 规模越来越大,代码越来越多,各种疑难杂症问题也随之出现。比较常见的一个问题就是 GC finalize() 方法出现 java.util.concurrent.TimeoutException,这类问题难查难解,困扰了很多开发者。那么这类问题是怎么出现的呢?有什么解决办法呢?这篇文章为将探索 finalize() timeout 的原因和解决方案,分享我们的踩坑经验,希望对遇到此类问题的开发者有所帮助。</p>
<p>在一些大型安卓 APP 中,经常会遇到一个奇怪的 BUG:ava.util.concurrent.TimeoutException</p>
<p>其表现为对象的 finalize() 方法超时,如 android.content.res.AssetManager.finalize() timed out after 10 seconds 。</p>
<p>此前滴滴出行安卓端曾长期受此 BUG 的影响,每天有一些用户会因此遇到 Crash,经过深度分析,最终找到有效解决方案。这篇文章将对这个 BUG 的来龙去脉以及我们的解决方案进行分析。 </p>
<p>▍问题详情</p>
<p>finalize() TimeoutException 发生在很多类中,典型的 Crash 堆栈如:</p>
<pre><code>1 java.util.concurrent.TimeoutException: android.content.res.AssetManager$AssetInputStream.finalize() timed out after 15 seconds
2 at android.content.res.AssetManager$AssetInputStream.close(AssetManager.java:559)
3 at android.content.res.AssetManager$AssetInputStream.finalize(AssetManager.java:592)
4 at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:187)
5 at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:170)
6 at java.lang.Thread.run(Thread.java:841)</code></pre>
<p>△ 左滑浏览全貌</p>
<p>这类 Crash 都是发生在 java.lang.Daemons$FinalizerDaemon.doFinalize 方法中,直接原因是对象的 finalize() 方法执行超时。系统版本从 Android 4.x 版本到 8.1 版本都有分布,低版本分布较多,出错的类有系统的类,也有我们自己的类。由于该问题在 4.x 版本中最具有代表性,下面我们将基于 AOSP 4.4 源码进行分析:</p>
<p>▍源码分析</p>
<p>首先从 Daemons 和 FinalizerDaemon 的由来开始分析,Daemons 开始于 Zygote 进程:Zygote 创建新进程后,通过 ZygoteHooks 类调用了 Daemons 类的 start() 方法,在 start() 方法中启动了 FinalizerDaemon,FinalizerWatchdogDaemon 等关联的守护线程。</p>
<pre><code> 1 public final class Daemons {
2 ...
3 private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND;
4
5 public static void start() {
6 FinalizerDaemon.INSTANCE.start();
7 FinalizerWatchdogDaemon.INSTANCE.start();
8 ...
9 }
10
11 public static void stop() {
12 FinalizerDaemon.INSTANCE.stop();
13 FinalizerWatchdogDaemon.INSTANCE.stop();
14 ...
15 }
16}</code></pre>
<p>△ 左滑浏览全貌</p>
<p>Daemons 类主要处理 GC 相关操作,start() 方法调用时启动了 5 个守护线程,其中有 2 个守护线程和这个 BUG 具有直接的关系。</p>
<p>▍FinalizerDaemon 析构守护线程</p>
<p>对于重写了成员函数finalize()的类,在对象创建时会新建一个 FinalizerReference 对象,这个对象封装了原对象。当原对象没有被其他对象引用时,这个对象不会被 GC 马上清除掉,而是被放入 FinalizerReference 的链表中。FinalizerDaemon 线程循环取出链表里面的对象,执行它们的 finalize() 方法,并且清除和对应 FinalizerReference对象引用关系,对应的 FinalizerReference 对象在下次执行 GC 时就会被清理掉。</p>
<pre><code> 1 private static class FinalizerDaemon extends Daemon {
2 ...
3 @Override public void run() {
4 while (isRunning()) {
5 // Take a reference, blocking until one is ready or the thread should stop
6 try {
7 doFinalize((FinalizerReference<?>) queue.remove());
8 } catch (InterruptedException ignored) {
9 }
10 }
11 }
12
13 @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
14 private void doFinalize(FinalizerReference<?> reference) {
15 ...
16 try {
17 finalizingStartedNanos = System.nanoTime();
18 finalizingObject = object;
19 synchronized (FinalizerWatchdogDaemon.INSTANCE) {
20 FinalizerWatchdogDaemon.INSTANCE.notify();
21 }
22 object.finalize();
23 } catch (Throwable ex) {
24 ...
25 } finally {
26 finalizingObject = null;
27 }
28 }
29}</code></pre>
<p>△ 左滑浏览全貌</p>
<p>▍FinalizerWatchdogDaemon 析构监护守护线程</p>
<p>析构监护守护线程用来监控 FinalizerDaemon 线程的执行,采用 Watchdog 计时器机制。当 FinalizerDaemon 线程开始执行对象的 finalize() 方法时,FinalizerWatchdogDaemon 线程会启动一个计时器,当计时器时间到了之后,检测 FinalizerDaemon 中是否还有正在执行 finalize() 的对象。检测到有对象存在后就视为 finalize() 方法执行超时,就会产生 TimeoutException 异常。</p>
<pre><code> 1 private static class FinalizerWatchdogDaemon extends Daemon {
2 ...
3 @Override public void run() {
4 while (isRunning()) {
5 ...
6 boolean finalized = waitForFinalization(object);
7 if (!finalized && !VMRuntime.getRuntime().isDebuggerActive()) {
8 finalizerTimedOut(object);
9 break;
10 }
11 }
12 }
13 ...
14 private boolean waitForFinalization(Object object) {
15 sleepFor(FinalizerDaemon.INSTANCE.finalizingStartedNanos, MAX_FINALIZE_NANOS);
16 return object != FinalizerDaemon.INSTANCE.finalizingObject;//当sleep时间到之后,检测 FinalizerDaemon 线程中当前正在执行 finalize 的对象是否存在,如果存在说明 finalize() 方法超时
17 }
18
19 private static void finalizerTimedOut(Object object) {
20 String message = object.getClass().getName() + ".finalize() timed out after "
21 + (MAX_FINALIZE_NANOS / NANOS_PER_SECOND) + " seconds";
22 Exception syntheticException = new TimeoutException(message);
23 syntheticException.setStackTrace(FinalizerDaemon.INSTANCE.getStackTrace());
24 Thread.UncaughtExceptionHandler h = Thread.getDefaultUncaughtExceptionHandler();
25 ...
26 h.uncaughtException(Thread.currentThread(), syntheticException);
27 }
28}</code></pre>
<p>△ 左滑浏览全貌</p>
<p>由源码可以看出,该 Crash 是在 FinalizerWatchdogDaemon 的线程中创建了一个TimeoutException 传给 Thread 类的 defaultUncaughtExceptionHandler 处理造成的。由于异常中填充了 FinalizerDaemon 的堆栈,之所以堆栈中没有出现和 FinalizerWatchdogDaemon 相关的类。</p>
<p>▍原因分析</p>
<p>finalize()导致的 TimeoutException Crash 非常普遍,很多 APP 都面临着这个问题。使用 finalize() TimeoutException 为关键词在搜索引擎或者 Stack Overflow 上能搜到非常多的反馈和提问,技术网站上对于这个问题的原因分析大概有两种:</p>
<p>▍对象 finalize() 方法耗时较长</p>
<p>当 finalize() 方法中有耗时操作时,可能会出现方法执行超时。耗时操作一般有两种情况,一是方法内部确实有比较耗时的操作,比如 IO 操作,线程休眠等。另外有种线程同步耗时的情况也需要注意:有的对象在执行 finalize() 方法时需要线程同步操作,如果长时间拿不到锁,可能会导致超时,如 android.content.res.AssetManager$AssetInputStream 类:</p>
<pre><code> 1 public final class AssetInputStream extends InputStream {
2 ...
3 public final void close() throws IOException {
4 synchronized (AssetManager.this) {
5 ...
6 }
7 }
8 ...
9 protected void finalize() throws Throwable {
10 close();
11 }
12 ...
13 }</code></pre>
<p>△ 左滑浏览全貌</p>
<p>AssetManager 的内部类 AssetInputStream 在执行 finalize() 方法时调用 close() 方法时需要拿到外部类 AssetManager 对象锁, 而在 AssetManager 类中几乎所有的方法运行时都需要拿到同样的锁,如果 AssetManager 连续加载了大量资源或者加载资源是耗时较长,就有可能导致内部类对象 AssetInputStream 在执行finalize() 时长时间拿不到锁而导致方法执行超时。</p>
<pre><code> 1 public final class AssetManager implements AutoCloseable {
2 ...
3 /*package*/ final CharSequence getResourceText(int ident) {
4 synchronized (this) {
5 ...
6 }
7 return null;
8 }
9 ...
10 public final InputStream open(String fileName, int accessMode) throws IOException {
11 synchronized (this) {
12 ...
13 }
14 throw new FileNotFoundException("Asset file: " + fileName);
15 }
16 ...
17 }</code></pre>
<p>△ 左滑浏览全貌</p>
<p>▍5.0 版本以下机型 GC 过程中 CPU 休眠导致</p>
<p>有种观点认为系统可能会在执行 finalize() 方法时进入休眠, 然后被唤醒恢复运行后,会使用现在的时间戳和执行 finalize() 之前的时间戳计算耗时,如果休眠时间比较长,就会出现 TimeoutException。</p>
<p>详情请见∞</p>
<p>确实这两个原因能够导致 finalize() 方法超时,但是从 Crash 的机型分布上看大部分是发生在系统类,另外在 5.0 以上版本也有大量出现,因此我们认为可能也有其他原因导致此类问题:</p>
<p>▍IO 负载过高</p>
<p>许多类的 finalize() 都需要释放 IO 资源,当 APP 打开的文件数目过多,或者在多进程或多线程并发读取磁盘的情况下,随着并发数的增加,磁盘 IO 效率将大大下降,导致 finalize() 方法中的 IO 操作运行缓慢导致超时。</p>
<p>▍FinalizerDaemon 中线程优先级过低</p>
<p>FinalizerDaemon 中运行的线程是一个守护线程,该线程优先级一般为默认级别 (nice=0),其他高优先级线程获得了更多的 CPU 时间,在一些极端情况下高优先级线程抢占了大部分 CPU 时间,FinalizerDaemon 线程只能在 CPU 空闲时运行,这种情况也可能会导致超时情况的发生,(从 Android 8.0 版本开始,FinalizerDaemon 中守护线程优先级已经被提高,此类问题已经大幅减少)</p>
<p>▍解决方案</p>
<p>当问题出现后,我们应该找到问题的根本原因,从根源上去解决。然而对于这个问题来说却不太容易实现,和其他问题不同,这类问题原因比较复杂,有系统原因,也有 APP 自身的原因,比较难以定位,也难以系统性解决。</p>
<p>▍理想措施</p>
<p>理论上我们可以做的措施有:</p>
<ol>
<li>减少对 finalize() 方法的依赖,尽量不依靠 finalize() 方法释放资源,手动处理资源释放逻辑。</li>
<li>减少 finalizable 对象个数,即减少有 finalize() 方法的对象创建,降低 finalizable 对象 GC 次数。</li>
</ol>
<p>3.finalize() 方法内尽量减少耗时以及线程同步时间。</p>
<ol><li>减少高优先级线程的创建和使用,降低高优先级线程的 CPU 使用率。</li></ol>
<p>▍止损措施</p>
<p>理想情况下的措施,可以从根本上解决此类问题,但现实情况下却不太容易完全做到,对一些大型APP来说更难以彻底解决。那么在解决问题的过程中,有没有别的办法能够缓解或止损呢?总结了技术网站上现有的方案后,可以总结为以下几种:</p>
<ul><li>手动修改 finalize() 方法超时时间</li></ul>
<pre><code>1 try {
2 Class<?> c = Class.forName(“java.lang.Daemons”);
3 Field maxField = c.getDeclaredField(“MAX_FINALIZE_NANOS”);
4 maxField.setAccessible(true);
5 maxField.set(null, Long.MAX_VALUE);
6 } catch (Exception e) {
7 ...
8 }</code></pre>
<p>△ 左滑浏览全貌</p>
<p><a href="https://link.segmentfault.com/?enc=tKYK9So6Fz2i2KV%2BxgE4zg%3D%3D.jntJ4w40WMGOlZ9ToKqTagBBD2U4zfSucXhaJ0iaOhvpwcHfuHceXsx8kuTjHH%2FPCjTqgqjguK80COOTuf760qcurHjchLoI5pSDdQ97ws%2BP0XOW9CVVbHCmEdwoJPDPnVYaFOiQCH82%2FcfQmo0Vev9XT6GuCjU4KKY2vofsu0s%3D" rel="nofollow">详情请见∞</a></p>
<p>这种方案思路是有效的,但是这种方法却是无效的。Daemons 类中 的 MAX_FINALIZE_NANOS 是个 long 型的静态常量,代码中出现的 MAX_FINALIZE_NANOS 字段在编译期就会被编译器替换成常量,因此运行期修改是不起作用的。MAX_FINALIZE_NANOS默认值是 10s,国内厂商常常会修改这个值,一般有 15s,30s,60s,120s,我们可以推测厂商修改这个值也是为了加大超时的阙值,从而缓解此类 Crash。</p>
<ul><li>手动停掉 FinalizerWatchdogDaemon 线程</li></ul>
<pre><code> 1 try {
2 Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
3 Method method = clazz.getSuperclass().getDeclaredMethod("stop");
4 method.setAccessible(true);
5 Field field = clazz.getDeclaredField("INSTANCE");
6 field.setAccessible(true);
7 method.invoke(field.get(null));
8 } catch (Throwable e) {
9 e.printStackTrace();
10 }</code></pre>
<p>△ 左滑浏览全貌</p>
<p><a href="https://link.segmentfault.com/?enc=6D8szL2%2FJQdls1mCQWn3IA%3D%3D.vRpKoZEfQ6QxQlBeaR4ty4bgGn3pfh6cxcP9SyTy02MLBkdId2Ej1EVJRvE%2BQOkhiUOiDDexdT%2FCuMiYHq4kFhNuSScbEXGn1pbjb0hHDftCznFPg1adRB3aeUVgT127bCthGRO8vcKZEITAppmGQVMqKfDXfYWevqGEAlGzTrg%3D" rel="nofollow">详情请见∞</a></p>
<p>这种方案利用反射 FinalizerWatchdogDaemon 的 stop() 方法,以使 FinalizerWatchdogDaemon 计时器功能永远停止。当 finalize() 方法出现超时, FinalizerWatchdogDaemon 因为已经停止而不会抛出异常。这种方案也存在明显的缺点:</p>
<ol>
<li>在 Android 5.1 版本以下系统中,当 FinalizerDaemon 正在执行对象的 finalize() 方法时,调用 FinalizerWatchdogDaemon 的 stop() 方法,将导致 run() 方法正常逻辑被打断,错误判断为 finalize() 超时,直接抛出 TimeoutException。</li>
<li>Android 9.0 版本开始限制 Private API 调用,不能再使用反射调用 Daemons 以及 FinalizerWatchdogDaemon 类方法。</li>
</ol>
<p>▍终极方案</p>
<p>这些方案都是阻止 FinalizerWatchdogDaemon 的正常运行,避免出现 Crash,从原理上还是具有可行性的:finalize() 方法虽然超时,但是当 CPU 资源充裕时,FinalizerDaemon 线程还是可以获得充足的 CPU 时间,从而获得了继续运行的机会,最大可能的延长了 APP 的存活时间。但是这些方案或多或少都是有缺陷的,那么有其他更好的办法吗?</p>
<p>What should we do? We just ignore it. </p>
<p>我们的方案就是忽略这个 Crash,那么怎么能够忽略这个 Crash 呢?首先我们梳理一下这个 Crash 的出现过程:</p>
<ol>
<li>FinalizerDaemon 执行对象 finalize() 超时。</li>
<li>FinalizerWatchdogDaemon 检测到超时后,构造异常交给 Thread 的 defaultUncaughtExceptionHandler 调用 uncaughtException() 方法处理。</li>
<li>APP 停止运行。</li>
</ol>
<p>Thread 类的 defaultUncaughtExceptionHandler 我们很熟悉了,Java Crash 捕获一般都是通过设置 Thread.setDefaultUncaughtExceptionHandler() 方法设置一个自定义的 UncaughtExceptionHandler ,处理异常后通过链式调用,最后交给系统默认的 UncaughtExceptionHandler 去处理,在 Android 中默认的 UncaughtExceptionHandler 逻辑如下:</p>
<pre><code> 1 public class RuntimeInit {
2 ...
3 private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
4 public void uncaughtException(Thread t, Throwable e) {
5 try {
6 ...
7 // Bring up crash dialog, wait for it to be dismissed 展示APP停止运行对话框
8 ActivityManagerNative.getDefault().handleApplicationCrash(
9 mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
10 } catch (Throwable t2) {
11 ...
12 } finally {
13 // Try everything to make sure this process goes away.
14 Process.killProcess(Process.myPid()); //退出进程
15 System.exit(10);
16 }
17 }
18 }
19
20 private static final void commonInit() {
21 ...
22 /* set default handler; this applies to all threads in the VM */
23 Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
24 ...
25 }
26 }</code></pre>
<p>△ 左滑浏览全貌</p>
<p>从系统默认的 UncaughtExceptionHandler 中可以看出,APP Crash 时弹出的停止运行对话框以及退出进程操作都是在这里处理中处理的,那么只要不让这个代码继续执行就可以阻止 APP 停止运行了。基于这个思路可以将这个方案表示为如下的代码:</p>
<pre><code> 1 final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
2 Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
3 @Override
4 public void uncaughtException(Thread t, Throwable e) {
5 if (t.getName().equals("FinalizerWatchdogDaemon") && e instanceof TimeoutException) {
6 //ignore it
7 } else {
8 defaultUncaughtExceptionHandler.uncaughtException(t, e);
9 }
10 }
11 });</code></pre>
<p>△ 左滑浏览全貌</p>
<ul><li>可行性</li></ul>
<p>这种方案在 FinalizerWatchdogDaemon 出现 TimeoutException 时主动忽略这个异常,阻断 UncaughtExceptionHandler 链式调用,使系统默认的 UncaughtExceptionHandler 不会被调用,APP 就不会停止运行而继续存活下去。由于这个过程用户无感知,对用户无明显影响,可以最大限度的减少对用户的影响。</p>
<ul><li>优点</li></ul>
<p>1.对系统侵入性小,不中断 FinalizerWatchdogDaemon 的运行。</p>
<p>2.Thread.setDefaultUncaughtExceptionHandler() 方法是公开方法,兼容性比较好,可以适配目前所有 Android 版本。</p>
<p>▍总结</p>
<p>不管什么样的缓解措施,都是治标不治本,没有从根源上解决。对于这类问题来说,虽然人为阻止了 Crash,避免了 APP 停止,APP 能够继续运行,但是 finalize() 超时还是客观存在的,如果 finalize() 一直超时的状况得不到缓解,将会导致 FinalizerDaemon 中 FinalizerReference 队列不断增长,最终出现 OOM 。因此还需要从一点一滴做起,优化代码结构,培养良好的代码习惯,从而彻底解决这个问题。当然 BUG 不断,优化不止,在解决问题的路上,缓解止损措施也是非常重要的手段。谁能说能抓老鼠的白猫不是好猫呢?</p>
<p>▍END<br>转载请至 / <a href="https://link.segmentfault.com/?enc=5lFeJ6%2Fmk3rNt6ogAS33KA%3D%3D.F2AvGXl7c%2Bb4VfQzxIGM%2B1eemWCwhy74%2B66LRGnM6quKW0awQIgWPTrHxe%2BU%2Bsll7makKHnEdG51Z8eN%2BrEXd9Fhvn74FtWhFplvOjQa2HbUEL91pF14eGF8y9bept5k" rel="nofollow">转载合作入口</a></p>
<p><img src="/img/bVbtdCB?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtr10?w=715&h=302" alt="图片描述" title="图片描述"></p>
<p>曾就职于奇虎360,长期从事移动端研发,2018年加入滴滴,专注于安卓移动端性能优化,架构演进,新技术探索,开源项目DroidAssist 作者。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
曾奇:谈谈我所认识的分布式锁
https://segmentfault.com/a/1190000019373045
2019-06-03T15:08:42+08:00
2019-06-03T15:08:42+08:00
滴滴技术
https://segmentfault.com/u/didijishu
22
<p><strong>出品 | 滴滴技术</strong><br><strong>作者 | 曾奇</strong></p>
<p><img src="/img/bVbtrLV?w=1880&h=800" alt="图片描述" title="图片描述"></p>
<p>前言:随着计算机技术和工程架构的发展,微服务变得越来越热。如今,绝大多数服务都处于分布式环境中,其中,数据一致性是我们一直关注的重点。分布式锁到底是什么?经过了哪些发展演进?工程上有哪些实现方案?各种方案的利弊权衡又有哪些?希望这篇文章能够对你有一些帮助。</p>
<p>▍阅读索引</p>
<p>0.名词定义<br>1.问题引入<br>2.分布式环境的特点<br>3.锁<br>4.分布式锁<br>5.分布式锁实现方案</p>
<ul>
<li>5.1朴素Redis实现方案、朴素Redis方案小结</li>
<li>5.2 ZooKeeper实现方案、ZooKeeper方案小结</li>
<li>5.3 Redisson实现方案、Redission方案小结</li>
</ul>
<p>6.总结<br>7.结束语<br>8.Reference</p>
<p>▍0. 名词定义</p>
<p>分布式锁:顾名思义,是指在分布式环境下的锁,重点在锁。所以我们先从锁开始讲起。</p>
<p>▍1. 问题引入</p>
<p>举个例子:</p>
<p>某服务记录数据X,当前值为100。A请求需要将X增加200;同时,B请求需要将X减100。 在理想的情况下,A先读取到X=100,然后X增加200,最后写入X=300。B请求接着读取到X=300,减少100,最后写入X=200。 然而在真实情况下,如果不做任何处理,则可能会出现:A和B同时读取到X=100;A写入之前B读取到X;B比A先写入等等情况。</p>
<p>上面这个例子相信大家都非常熟悉。出现不符合预期的结果本质上是对临界资源没有做好互斥操作。互斥性问题通俗来讲,就是对共享资源的抢占问题。对于共享资源争抢的正确性,锁是最常用的方式,其他的如CAS(compare and swap)等,这里不展开。</p>
<p>▍2. 分布式环境的特点</p>
<p>我们的绝大部分服务都处于分布式环境中。那么,分布式系统有哪些特点呢?大致如下:</p>
<ul>
<li>可扩展性:可通过横向水平扩展提高系统的性能和吞吐量。</li>
<li>高可靠性:高容错,即使系统中一台或几台故障,系统仍可提供服务。</li>
<li>高并发性:各机器并行独立处理和计算。</li>
<li>廉价高效:多台小型机而非单台高性能机。</li>
</ul>
<p>▍3.锁</p>
<p>我们先来看下非分布式情况下的锁方案(多线程和多进程的情况),然后再演进到分布式锁。</p>
<p>▍多线程下的锁机制:</p>
<p>各种语言有不同的实现方式,比较成熟。比如,go语言中的sync.RWMutex(读写锁)、sync.Mutex(互斥锁);JAVA中的ReentrantLock、synchronized;在php中没有找到原生的支持锁的方式,只能通过外部来间接实现,比如文件锁,借助外部存储的锁等。</p>
<p>▍多进程下的锁机制:</p>
<p>对于临界资源的访问已经超出了单个进程的控制范围。在多进程的情况下,主要是利用操作系统层面的进程间通信原理来解决临界资源的抢占问题。比较常见的一种方法便是使用信号量(Semaphores)。</p>
<p>▍对信号量的操作,主要是P操作(wait)和V操作(signal):</p>
<ul><li>P操作 ( wait ) :</li></ul>
<p>先检查信号量的大小,若值大于零,则将信号量减1,同时进程获得共享资源的访问权限,继续执行;若小于或者等于零,则该进程被阻塞后,进入等待队列。</p>
<ul><li>V操作 ( signal ) :</li></ul>
<p>该操作将信号量的值加1,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。</p>
<p>可看出,多进程锁方案跟多线程的锁方案实现思路大同小异。</p>
<p>我们将互斥的级别拉高,分布式环境下不同节点不同进程或线程之间的互斥,就是分布式锁的挑战之一。后面再细讲。</p>
<p>另外,在传统的基于数据库的架构中,对于数据的抢占问题也可以通过数据库事务(ACID)来保证。在分布式环境中,出于对性能以及一致性敏感度的要求,使得分布式锁成为了一种比较常见而高效的解决方案。</p>
<p>▍从上面对于多线程和多进程锁的概括,可以总结出锁的抽象条件:</p>
<p>1)“需要有存储锁的空间,并且锁的空间是可以访问到的”:</p>
<p>对于多线程就是内存(进程中不同的线程都可以读写),多进程中通过共享内存的方式,也是提供一块地方,供不同进程读写。主要目的是保证不同的进线程改动对于其他进线程可见,进而满足互斥性需求。</p>
<p>2)“锁需要被唯一标识”:</p>
<p>不同的共享资源,必然需要用不同的锁进行保护,因此相应的锁必须有唯一的标识。在多线程环境中,锁可以是一个对象,那么对这个对象的引用便是这个唯一标识。多进程下,比如有名信号量,便是用硬盘中的文件名作为唯一标识。</p>
<p>3)“锁要有至少两种状态”:</p>
<p>有锁,没锁。存在,不存在等等。很好理解。</p>
<p>满足上述三个条件就可以实现基础的分布式锁了。但是随着技术的演进,</p>
<p>▍相应地,对锁也提出了更高级的条件:</p>
<p>1)可重入:</p>
<p>外层函数获得锁之后,内层函数还可以获得锁。原因是随着软件复杂性增加,方法嵌套获取锁已经很难避免。但是从代码层面很难分析出这个问题,因此我们要使用可重入锁。导致锁需要支持可重入的场景。对于可重入的思考,每种语言有自己的哲学和取舍,如go就舍弃了支持重入:Recursive locking in Go [ <a href="https://link.segmentfault.com/?enc=hxEmOAKveQAcXedqi%2B3%2Fyw%3D%3D.GnlSvkn1lugbn4LOCcRdzyjogE%2FfpJvtbJogHxdSLeiT7Hw%2BvhAXq5y%2Fu1%2FpPkbnU24b1h0SYv2lo6IMBM2zF4KDMJZi1tZksG67MONCA%2B4%3D" rel="nofollow">https://stackoverflow.com/que...</a> ]以后go又会不会认为“可重入真香”呢?哈哈,我们拭目以待。</p>
<p>2)避免产生惊群效应(Herd Effect):</p>
<p>惊群效应指,在有多个请求等待获取锁的时候,一旦占有锁的线程释放之后,所有等待方都同时被唤醒,尝试抢占锁。但是绝大多数的抢占都是不必要的。这种情况在多线程和多进程中开销同样很大。要尽量避免这种情况出现。</p>
<p>3)公平锁和非公平锁:</p>
<p>公平锁:优先把锁给等待时间最长的一方;非公平锁:不保证等待线程拿锁的顺序。公平锁的实现成本较高。</p>
<p>4)阻塞锁和自旋锁:</p>
<p>主要是效率的考虑。自旋锁适用于临界区操作耗时短的场景;阻塞锁适用于临界区操作耗时长的场景。</p>
<p>5)锁超时:</p>
<p>防止释放锁失败,出现死锁的情况。</p>
<p>6)高效,高可用:</p>
<p>加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。</p>
<p>还有很多其他更高要求的条件,不一一列举了。有兴趣的小伙伴可以看看编程史上锁的演进过程。</p>
<p>▍4. 分布式锁</p>
<p>▍使用分布式锁的必要性:</p>
<p>1)服务要求:部署的服务本身就处于分布式环境中</p>
<p>2)效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信</p>
<p>3)正确性:跟2)类似。如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失</p>
<p>包括但不限于这些必要性,在强烈地呼唤我们今天的主角---“分布式锁”闪亮登场。</p>
<p>▍5. 分布式锁实现方案</p>
<p>有了非分布式锁的实现思路,和分布式环境的挑战,我们来看看分布式锁的实现策略。<br>分布式锁本质上还是要实现一个简单的目标---占一个“坑”,当别的节点机器也要来占时,发现已经有人占了,就只好放弃或者稍后再试。</p>
<p>▍大体分为4种</p>
<p>1)使用数据库实现<br>2)使用朴素Redis等缓存系统实现<br>3)使用ZooKeeper等分布式协调系统实现<br>4)使用Redisson来实现(本质上基于Redis)</p>
<p>因为利用mysql实现分布式锁的性能低以及改造大,我们这里重点讲一下下面3种实现分布式锁的方案。</p>
<p>▍5.1 朴素Redis实现方案</p>
<p>我们循序渐进,对比几种实现方式,找出优雅的方式:</p>
<p>方案1:setnx+delete</p>
<pre><code>1 setnx lock_key lock_value
2 // do sth
3 delete lock_key</code></pre>
<p>缺点:一旦服务挂掉,锁无法被删除释放,会导致死锁。硬伤,pass!2</p>
<p>方案2:setnx + setex</p>
<pre><code>1 setnx lock_key lock_value
2 setex lock_key N lock_value // N s超时
3 // do sth
4 delete lock_key</code></pre>
<p>在方案1的基础上设置了超时时间。但是还是会出现跟1一样的问题。如果setnx之后、setex之前服务挂掉,一样会陷入死锁。本质原因是,setnx/setex分为了两个步骤,非原子操作。硬伤,pass!</p>
<p>方案3:set ex nx</p>
<pre><code>1 SET lock_key lock_value EX N NX //N s超时
2 // do sth
3 delete lock_key</code></pre>
<p>将加锁、设置超时两个步骤合并为一个原子操作,从而解决方案1、2的问题。(Redis原生命令支持,Redis version需要>=2.6.12,滴滴生产环境Redis version一般为3.2,所以日常能够使用)。</p>
<p>优点:此方案目前大多数sdk、Redis部署方案都支持,实现简单<br>缺点:会存在锁被错误的释放,被错误的抢占的情况。如下图:</p>
<p><img src="/img/bVbtrRz?w=1228&h=668" alt="图片描述" title="图片描述"></p>
<p>这块有2个问题:</p>
<p>1)GC期间,client1超时时间已到,导致将client2错误地放进来</p>
<p>2)client1执行完逻辑后会显式调用del,将所有的锁都释放了(正确的情况应该只释放自己的锁,错误地释放了client2的锁)</p>
<p>方案4:</p>
<p>在3的基础上,对于问题1,将client的超时时间设置长一些,保证只能通过显式del来释放锁,而超时时间只是作为一种最终兜底的方案。针对问题2,增加对 value 的检查,只解除自己加的锁,为保证原子性,只能需要通过lua脚本实现。</p>
<p>lua脚本:<a href="https://link.segmentfault.com/?enc=hK2jgwnkH3fIVDumo637Zw%3D%3D.C17OSbxV%2BDAdiryAVRfVTwl36Hts1ruQkk63ZsD55GM%3D" rel="nofollow">https://redis.io/commands/eval</a></p>
<pre><code>1 if redis.call("get",KEYS[1]) == ARGV[1] then
2 return redis.call("del",KEYS[1])
3 else
4 return 0
5 end</code></pre>
<p>如果超时时间设置长,只能通过显式的del来释放锁,就不会出现问题2(错误释放掉其他client的锁)。跟滴滴KV store的王斌同学讨论过,目前没有找到方案4优于方案3(只要超时时间设置的长一些)的场景。所以,在我的认知中,方案4跟方案3的优势一样,但是方案3的实现成本明显要低很多。</p>
<p>朴素Redis方案小结</p>
<p>方案3用的最多,实现成本小,对于大部分场景,将超时时间设置的长一些,极少出现问题。同时本方案对不同语言的友好度极高。</p>
<p>▍5.2 ZooKeeper实现方案</p>
<p>我们先简要介绍一些ZooKeeper(以下简称ZK):</p>
<p>ZooKeeper是一种“分布式协调服务”。所谓分布式协调服务,可以在分布式系统中共享配置,协调锁资源,提供命名服务等。为读多写少的场景所设计,ZK中的节点(以下简称ZNode)非常适合用于存储少量的状态和配置信息。</p>
<p>对ZK常见的操作:</p>
<p>create:创建节点<br>delete:删除节点<br>exists:判断一个节点的数据<br>setdata:设置一个节点的数据<br>getchildren:获取节点下的所有子节点</p>
<p>这其中,exists,getData,getChildren属于读操作。Zookeeper客户端在请求读操作的时候,可以选择是否设置Watch(监听机制)。</p>
<p>什么是Watch?</p>
<p>Watch机制是zk中非常有用的功能。我们可以理解成是注册在特定Znode上的触发器。当这个Znode发生改变,也就是调用了create,delete,setData方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接收到异步通知。<br>我们在实现分布式锁的时候,正是通过Watch机制,来通知正在等待的session相关锁释放的信息。</p>
<p>什么是ZNode?</p>
<p>ZNode就是ZK中的节点。ZooKeeper节点是有生命周期的,这取决于节点的类型。在 ZooKeeper 中,节点类型可以分为临时节点(EPHEMERAL),时序节点(SEQUENTIAL ),持久节点(PERSISTENT )。</p>
<p>临时节点(EPHEMERAL):</p>
<p>节点的生命周期跟session绑定,session创建的节点,一旦该session失效,该节点就会被删除。</p>
<p>临时顺序节点(EPHEMERAL_SEQUENTIAL):</p>
<p>在临时节点的基础上增加了顺序。每个父结点会为自己的第一级子节点维护一份时序。在创建子节点的时候,会自动加上数字后缀,越后创建的节点,顺序越大,后缀越大。</p>
<p>持久节点(PERSISTENT ):</p>
<p>节点创建之后就一直存在,不会因为session失效而消失。</p>
<p>持久顺序节点(PERSISTENT_SEQUENTIAL):</p>
<p>与临时顺序节点同理。</p>
<p>ZNode中的数据结构:</p>
<p>data(znode存储的数据信息),acl(记录znode的访问权限,即哪些人或哪些ip可以访问本节点),stat(包含znode的各种元数据,比如事务id,版本号,时间戳,大小等等),child(当前节点的子节点引用)。</p>
<p>利用ZK实现分布式锁,主要得益于ZK保证了数据的强一致性。</p>
<p>下面说说通过zk简单实现一个保持独占的锁(利用临时节点的特性):</p>
<p>我们可以将ZK上的ZNode看成一把锁(类似于Redis方案中的key)。多个session都去创建同一个distribute_lock节点,只会有一个创建成功的session。相当于只有该session获取到锁,其他session没有获取到锁。在该成功获锁的session失效前,锁将会一直阻塞住。session失效时,节点会自动被删除,锁被解除。(类似于Redis方案中的expire)。</p>
<p>上述实现方案跟Redis方案3的实现效果一样。</p>
<p>但是,这样的锁有没有改进的地方?当然!</p>
<p>1)我们可能会有可重入的需求,因此希望能有可重入的锁机制。</p>
<p>2)有些场景下,在争抢锁的时候,我们既不想一次争抢不到就pass,也不想一直阻塞住直到获取到锁。一个朴素的需求是,我们希望有超时时间来控制是否去上锁。更进一步,我们不想主动的去查到底是否能够加锁,我们希望能够有事件机制来通知是否能够上锁。(这里,你是不是想到了ZK的Watch机制呢?)</p>
<p>要满足这样的需求就需要控制时序。利用顺序临时节点和Watch机制的特性,来实现:</p>
<p>我们事先创建/distribute_lock节点,多个session在它下面创建临时有序节点。由于zk的特性,/distribute_lock该节点会维护一份sequence,来保证子节点创建的时序性。</p>
<p>具体实现如下:</p>
<p>1)客户端调用create()方法在/distribute_lock节点下创建EPHEMERAL_SEQUENTIAL节点。</p>
<p>2)客户端调用getChildren(“/distribute_lock”)方法来获取所有已经创建的子节点。</p>
<p>3)客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点序号最小,那么就认为这个客户端获得了锁。</p>
<p>4)如果在步骤3中发现自己并非所有子节点中最小的,说明自己还没有获取到锁。此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时注册事件监听。需要注意是,只在比自己小一号的节点上注册Watch事件。如果在比自己都小的节点上注册Watch事件,将会出现惊群效应,要避免。</p>
<p>5)之后当这个被关注的节点被移除了,客户端会收到相应的通知。这个时候客户端需要再次调用getChildren(“/distribute_lock”)方法来获取所有已经创建的子节点,确保自己确实是最小的节点了,然后进入步骤3)。</p>
<p>Curator框架封装了对ZK的api操作。以Java为例来进行演示:<br>引入依赖:</p>
<pre><code>1 <dependency>
2 <groupId>org.apache.curator</groupId>
3 <artifactId>curator-recipes</artifactId>
4 <version>2.11.1</version>
5 </dependency></code></pre>
<p>使用的时候需要注意Curator框架和ZK的版本兼容问题。<br>以排他锁为例,看看怎么使用:</p>
<pre><code> 1 public class TestLock {
2
3 public static void main(String[] args) throws Exception {
4 //创建zookeeper的客户端
5 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
6 CuratorFramework client = CuratorFrameworkFactory.newClient(“ip:port", retryPolicy);
7 client.start();
8
9 //创建分布式锁, 锁空间的根节点路径为/sunnyzengqi/curator/lock
10 InterProcessMutex mutex = new InterProcessMutex(client, "/sunnyzengqi/curator/lock");
11 mutex.acquire();
12 //获得了锁, 进行业务流程
13 System.out.println("Enter mutex");
14 Thread.sleep(10000);
15 //完成业务流程, 释放锁
16 mutex.release();
17 //关闭客户端
18 client.close();
19 }
20 }
21</code></pre>
<p>△左滑浏览全貌</p>
<p>上面代码在业务执行的过程中,在ZK的/sunnyzengqi/curator/lock路径下,会创建一个临时节点来占位。相同的代码,在两个机器节点上运行,可以看到该路径下创建了两个临时节点:</p>
<p><img src="/img/bVbtrSy?w=1108&h=108" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrSH?w=1107&h=323" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrSI?w=1109&h=321" alt="图片描述" title="图片描述"></p>
<p>运行命令echo wchc | nc localhost 2181查看watch信息:</p>
<p><img src="/img/bVbtrSR?w=1108&h=71" alt="图片描述" title="图片描述"></p>
<p>可以看到lock1节点的session在监听节点lock0的变动。此时是lock0获取到锁。等到lock0执行完,session会失效,触发Watch机制,通知lock1的session说锁已经被释放了。这时,lock1可以来抢占锁,进而执行自己的操作。</p>
<p>除了简单的排它锁的实现,还可以利用ZK的特性来实现更高级的锁(比如信号量,读写锁,联锁)等,这里面有很多的玩法。</p>
<p>ZooKeeper方案小结</p>
<p>能够实现很多具有更高条件的锁机制,并且由于ZK优越的session和watch机制,适用于复杂的场景。因为有久经检验的Curator框架,集成了很多基于ZK的分布式锁的api,对于Java语言非常友好。对于其他语言,虽然也有一些开源项目封装了它的api,但是稳定性和效率需要自己去实际检验。</p>
<p>▍5.3 Redisson实现方案</p>
<p>我们先简要介绍一下Redisson:</p>
<p>Redisson是Java语言编写的基于Redis的client端。功能也非常强大,功能包括:分布式对象,分布式集合,分布式锁和同步器,分布式服务等。被大家熟知的场景还是在分布式锁的场景。</p>
<p>为了解决加锁线程在没有解锁之前崩溃进而出现死锁的问题,不同于朴素Redis中通过设置超时时间来处理。Redisson采用了新的处理方式:Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。<br>跟Zookeeper类似,Redisson也提供了这几种分布式锁:可重入锁,公平锁,联锁,红锁,读写锁等。具体怎么用这里不展开,感兴趣的朋友可以自己去实验。</p>
<p>Redisson方案小结</p>
<p>跟ZK一样,都能够实现很多具有更高条件的锁机制,适用于复杂的场景。但对语言非常挑剔,目前只能支持Java语言。</p>
<p>▍6. 总结</p>
<p>上一节,我们讨论了三种实现的方案:朴素Redis实现方案,ZooKeeper实现方案,Redisson实现方案。由于第1种与第3种都是基于Redis,所以主要是ZK和基于Redis两种。我们不禁想问,在实现分布式锁上,基于ZK与基于Redis的方案,有什么不同呢?</p>
<p>1)锁的时长设置上:</p>
<p>得益于ZK的session机制,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。避免了基于Redis的锁对于有效时间到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。</p>
<p>优势:ZK>Redisson>朴素Redis。</p>
<p>2)监听机制上:</p>
<p>得益于ZK的watch机制,在获取锁失败之后可以等待锁重新释放的事件。这让客户端对锁的使用更加灵活。避免了Redis方案主要去轮询的方式。</p>
<p>优势:ZK>Redisson=朴素Redis。</p>
<p>3)使用便利性上:</p>
<p>由于生产环境都有稳定的Redis和ZooKeeper集群,有专业的同学维护,这两者差别不大。在语言局限性上,朴素Redis从不挑食。ZK和Redisson都偏向于Java语言。在开发难度上,Redis最简单,几乎不用写什么代码;ZK和Redisson次之,依赖于使用的语言是否有集成的api以及集成稳定性等。</p>
<p>优势:朴素Redis>ZK>Redisson。</p>
<p>4)支持锁形式的多样性上:</p>
<p>上面有提及,ZK和Redisson都支持了各种花样的分布锁。朴素Redis就比较捉急了,在实现更高要求的锁方面,如果自己造轮子,往往费时费力,力不从心。</p>
<p>优势:ZK=Redisson>Redis。</p>
<p>▍7. 结束语</p>
<p>分布式锁在日常Coding中已经很常用。但是分布式锁这方面的知识依然非常深奥。2016年,Martin Kleppmann与Antirez两位分布式领域非常有造诣的前辈还针对“Redlock算法”在分布式锁上面的应用炒得沸沸扬扬。</p>
<p><em>最后借助这场历史闹剧中Martin的话来结束我们今天的分享。与诸君共勉!将学习当成一生的主题!</em></p>
<p><em>对我来说最重要的一点在于:我并不在乎在这场辩论中谁对谁错 —— 我只关心从其他人的工作中学到的东西,以便我们能够避免重蹈覆辙,并让未来更加美好。前人已经为我们创造出了许多伟大的成果:站在巨人的肩膀上,我们得以构建更棒的软件。</em><br>……<br><em>对于任何想法,务必要详加检验,通过论证以及检查它们是否经得住别人的详细审查。那是学习过程的一部分。但目标应该是为了获得知识,而不应该是为了说服别人相信你自己是对的。有时候,那只不过意味着停下来,好好地想一想。</em></p>
<p>由于时间仓促,自己水平有限,文中必定存在诸多疏漏与理解不当的地方。非常希望得到各位指正,畅谈技术。</p>
<p>▍Reference</p>
<p><a href="https://link.segmentfault.com/?enc=OtG6k0INrTtC6zsOFBuKyg%3D%3D.6fzED5EJHP0qUmUOnURMnk1NOQCyZcEaEGyQD3Ro0DQ%3D" rel="nofollow">0.Apache ZooKeeper</a><br><a href="https://link.segmentfault.com/?enc=hB3v52qzvm9JmUXi4ik0zA%3D%3D.gepzZG2zWA8EFf%2BrTz5tcLQzcK8UW7cs9cegVEOutVcjVObXg3x23wJqaaCGEr9g" rel="nofollow">1.Redisson</a><br><a href="https://link.segmentfault.com/?enc=DUTTxeiOGogwxjbsq9OKsA%3D%3D.MRemoKoBt62vG5yd97DPUoM4u3mHTmDGXHT7QaX1XMk%3D" rel="nofollow">2.Redis</a><br><a>3.Redis分布式锁进化史</a><br><a href="https://link.segmentfault.com/?enc=%2FoqymYgp6B0yFaeimRi1AA%3D%3D.edmVeW%2F3zM3cbX2Q6hct%2FIGI53ujFTfga7GlV02ScP4QNBcJ%2B9u0LTeNHsv%2BhWfmlTt52Jku7CzpF9LrxbAN8dA5mUD5VVqE3kKEBBUZCWm9L6qvwe0TGgSm5A8u6Q1JjliORw74XRns9lZHD3gzpA%3D%3D" rel="nofollow">4.分布式系统互斥性与幂等性问题的分析与解决</a><br>5.浅谈可重入性及其他<br><a href="https://link.segmentfault.com/?enc=Z2gfoPyI4L4NaFB82LduhQ%3D%3D.VblyLUgI1PmV5l4QfRgEeXVCWW6AxDL6S6dq0JHSHAw9sPVLm2NDsifnCELL2iCh" rel="nofollow">6.Distributed locks with Redis</a><br><a href="https://link.segmentfault.com/?enc=wu9kCLisqryjj5nc20pshA%3D%3D.eFBqvBNFJMrKPDE70FyZsASL4Z86GgEgzRCvSAPm%2Fk1K08heQIwYNb9KsdzUCkjVGcV42IDYblMT8al7jJ3A2hhlXwBlDQ5t4IWBNNjxpYY%3D" rel="nofollow">7.How to do distributed locking</a><br><a href="https://link.segmentfault.com/?enc=a3M32Y%2FXIzLTAOynT6hCzQ%3D%3D.5OeBbc5afoa37q2vN%2FKiC1M3gWJrUp03AkNUn0EEf3U%3D" rel="nofollow">8.Is Redlock safe?</a><br>9.Note on fencing and distributed locks</p>
<p>▍END<br>转载请至 / <a href="https://link.segmentfault.com/?enc=DlDWHvzjdFCtCw25ZBaqyg%3D%3D.4j2jT9HEcBxftufReJKY2GeF65LbthLi9JpiHuYAH3xFVv1mTeap5ZzB%2FZuZ1ENToEGbm5XVvJtYkfzLIc8yYPaEbcdKOs3YvC79eSsHpfqlPCMDl9XZo5cDWWCpo5Gf" rel="nofollow">转载合作入口</a></p>
<p><img src="/img/bVbtdCB?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrXt?w=817&h=307" alt="图片描述" title="图片描述"></p>
<p>北京科技大学本硕,2018年应届入职滴滴。热爱技术,更热爱用技术去解决实际问题。对分布式系统,大型网站架构有一定的了解。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
胡海洋:Hive Metastore Federation 在滴滴的实践
https://segmentfault.com/a/1190000019370850
2019-06-03T12:02:46+08:00
2019-06-03T12:02:46+08:00
滴滴技术
https://segmentfault.com/u/didijishu
3
<p>出品 | 滴滴技术<br>作者 | 胡海洋</p>
<p><img src="/img/bVbtri7?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>前言:本文来自滴滴基础平台大数据架构离线引擎组,针对内部hive元数据上亿级别存储方案的实践;该架构体系从根本上提高了hive元数据服务的稳定性及扩展性问题。</p>
<p>▍背景</p>
<p>Apache Hive 是基于 Apache Hadoop 之上构建的数据仓库,提供了简单易用的类 SQL 查询语言,适合对大规模数据进行存储、查询操作,被广泛使用。</p>
<p>Hive 元数据 Metadata 包含用 Hive 创建的 Database、Table、Partition 等的元信息。此类元信息一般存储在关系型数据库中,如 Derby、MySQL 等。</p>
<p>在滴滴,我们是将元数据存储在MySQL 里,随着业务的不断增长,Hive 元数据越来越庞大,出现单表存储超过上亿条记录的情况,这种情况下会导致 MySQL查询压力过大,而且在高峰期间,经常导致机器 CPU 使用率达到 100%,影响服务稳定性。</p>
<p>为此,我们开始调研元数据 Federation 方案,实现元数据水平扩展能力,为 MySQL 解压,提升 Hive 稳定性。</p>
<p>▍Federation 方案介绍</p>
<p>▍2.1 Hive 架构体系演变</p>
<p>引入 Federation 之前的 Hive 架构体系:</p>
<p><img src="/img/bVbtrjB?w=1412&h=1080" alt="图片描述" title="图片描述"></p>
<p>该架构体系中用户使用的 Hive 客户端或者 Hivesever2 服务、Spark 引擎、Presto 引擎等都是访问统一 Hive Metastore 服务获取 Hive 元数据。</p>
<p>Hive Metastore 服务主要是使用 LVS + 多个 Hive Metastore 实例组成。所有的 Hive Metastore 实例共享一套主从 MySQL 环境作为 Hive 元数据存储 DB。</p>
<p>▍前期工作</p>
<p>为了缓解 Hive 元数据存储 DB(MySQL)查询压力,我们做了很多元数据治理工作,包括长时间无用的库、表、分区清理等。元数据访问限制包括查询分区表分区限制等功能,但这些并没有从根本上解决 MySQL 压力问题。</p>
<p>▍方案调研</p>
<p>MySQL 机器查询压力大的问题主要由于 Hive 元数据结构中只存在一个库,库中存在单表超过上亿条记录的情况。那为什么不考虑 MySQL 分库,分表等方案?如果采用 MySQL 分库,分表等方案,需要大量更改 Hive Metastore 接口,这样风险及成本较高,而且后期涉及 Hive 版本升级也会带来更多工作量。</p>
<p>基于 Hive Metastore 层实现 Federation(waggle_dance),实现思路主要是以 Hive DB 切分,在 Hive DB 层面将元数据分布多套 MySQL 环境存储,这样对 Hive Metastore 本身不需要做任何修改,这种方案也较好维护。</p>
<p>基于 Hive Metastore Federation 实现后的 Hive 架构体系:</p>
<p><img src="/img/bVbtrjJ?w=1434&h=1104" alt="图片描述" title="图片描述"></p>
<p>▍2.2 waggle_dance 介绍</p>
<p>Reference:<br><a href="https://link.segmentfault.com/?enc=TfbveHOTQu1ouahnFK8WyA%3D%3D.H0Rj9osWEk2c6FIWudzAyzK8SOyMCHjacUcr7HttrKx02ZXih9LLidbTtZ%2BXCgfepWN3rPNiVd0L%2B6jn8iFT5lKt62Ozem1s6%2B%2FcmDBqql0%3D" rel="nofollow">https://cwiki.apache.org/conf...</a><br><a href="https://link.segmentfault.com/?enc=38Q4GBmr49g3pLDO6KF2QQ%3D%3D.7O1wVwcwt6kIx93KKij1lZOgN2s6ZoP18WJ4b5jn5Iz9yq8XhctI7RobtIYpb5fw" rel="nofollow">https://github.com/HotelsDotC...</a></p>
<p>waggle-dance 是由 Hotels.com 公司开源的一个项目,该项目主要是联合多个 Hive Metastore 数据查询的服务,实现了一个统一的路由接口解决多套 Hive Metastore 环境间的元数据共享问题。</p>
<p>▍2.2.1 架构流程图</p>
<p><img src="/img/bVbtrjQ?w=916&h=512" alt="图片描述" title="图片描述"></p>
<p>从架构图来看, waggle_dance 服务其实是承担 Router 路由的角色,后端配置多个 Hive Metastore 环境,接收客户端的元数据请求,通过 DB 与 Hive Metastore 路由关系将请求具体转发到相应的 Hive Metastore 环境执行操作。</p>
<p>这些操作对于客户端来说完全透明,对于客户端只是访问一套 Hive Metastore 的环境。</p>
<p>▍2.2.2 内部组件解析</p>
<p>waggle_dance 基于 Spring-boot 框架实现,主要包括如下几个模块:</p>
<p><img src="/img/bVbtrjW?w=778&h=278" alt="图片描述" title="图片描述"></p>
<p>▍WaggleDance容器</p>
<p>服务启动类,主要初始化容器 Spring boot,加载 Listener,每个关键的类通过注解方式加载,初始化。</p>
<p>▍YamlFederatedMetaStoreStorage 模块</p>
<p>维护需要依赖 Hive Metastore 环境的配置信息及 Hive Database 名字到 Hive Metastore 服务之间的路由信息。</p>
<p>当前支持配置一个主 Hive Metastore 及多个从 Hive Metastore 策略。</p>
<p>主 Hive Metastore 与从 Hive Metastore 配置主要区分的属性 access-control-type。</p>
<p><img src="/img/bVbtrj4?w=1300&h=406" alt="图片描述" title="图片描述"></p>
<ul>
<li>主 Hive Metastore 定义属性 access-control-type:对当前环境的 Hive 元数据操作支持以上 4<br> 种策略。</li>
<li>从 Hive Metastore 定义属性 access-control-type:对当前环境的 Hive 元数据操作只支持<br> READ_ONLY 只读策略。</li>
</ul>
<p>▍文件配置管理模块</p>
<ul>
<li>WaggleDanceConfiguration: ThriftServer 服务属性配置</li>
<li>GraphiteConfiguration: Graphite 监控属性配置</li>
</ul>
<p>▍ThriftServer 模块</p>
<p>实现 ThriftServer 服务,基于 Hive ThriftHiveMetastore API 对外提供 RPC 服务。接口包括 create_database、drop_database、create_table 以及汇总信息查询如 get_all_databases、set_ugi 等操作。</p>
<p>这样可以做到完全兼容 Hive 客户端,Hivesever2 等服务请求协议,最终通过路由解析至对应的 Hive Metastore 建立连接并调用同名方法执行操作。</p>
<p>几个需要注意的地方:</p>
<ul>
<li>接口的操作是区分只读和读写等策略,会根据路配置文件中定义的 Hive Metastore 的 access-control-type<br> 属性决定。</li>
<li>对于那些无法通过库名来路由的接口,都是转发至主 Hive Metastore 环境执行操作。</li>
</ul>
<p>▍Monitor 模块</p>
<ul><li>MonitoringConfiguration、MonitoredAspect:服务监控逻辑,主要采用 Java 监控类库 Metrics<br> 实现(项目官网 <a href="https://link.segmentfault.com/?enc=w8R3wi6h%2BNUsoTVVNR%2BkvQ%3D%3D.H015u2n18Oixw49xR3hvrTD6HcM4400BO5ryhAcjztg%3D" rel="nofollow">http://metrics.dropwizard.io/</a> ),该库支持将相关监控信息通过 Ganglia 和<br> Graphite 等工具进行展示,主要对 JVM 内存,线程执行状态,Hive 元数据相关操作等监控。</li></ul>
<p>▍FederationsAdmin 管理模块</p>
<ul><li>FederationsAdminController:restful 接口实现,供管理员使用,可动态完成对 Federation<br> Metastore 注册、注销及 Hive Database 名字与 Metastore 路由信息修改。</li></ul>
<p>▍滴滴实践</p>
<p>▍3.1 服务改造</p>
<p>当前 waggle_dance 功能不能完全满足我们的使用要求,需要进行扩展。改进点如下:</p>
<ul>
<li>目标是需要支持对多套 Hive Metastore 环境进行读写元数据操作,所以扩展<br> waggle-dance-federation.yml 文件模板支持配置多个主 Hive Metastore。</li>
<li>MappingEventListener 增加 MULTI_PRIMARY 策略。根据多个主 Hive Metastore 模式实现对应的<br> Hive Database 名字->Metastore mapping 路由信息类。</li>
<li>修改 FederatedHMSHandler 等类相关处理逻辑(兼容 Hive 1.2.1 与 2.x 版本 Hive<br> ThriftHiveMetastore API)。这样做的好处是在客户端保持 Hive 1.2.1 版本的情况下可以完全兼容后端多个<br> Hive 版本 Metastore 服务,方便后期对 Metastore 服务版本升级。</li>
<li>修改监控模块,由于公司内部是使用 Ganglia 工具进行监控,将 Graphite 改造为 Ganglia 并优化监控指标。</li>
<li>相关性能优化,为了避免每个客户端连接过程中都需要建立一个 Database->Metastore mapping<br> 路由信息耗费的内存操作,每个 waggle_dance 实例启动的时候缓存一个 Hive Database->Metastore<br> mapping 路由信息,该信息会被每个客户端连接请求的线程共用。</li>
</ul>
<p>对于 Database->Metastore mapping 路由信息的维护,每个 waggle_dance 实例会定期请求后端多套 Hive Metastore 服务进行数据更新。<br>改造后 waggle_dance 架构流程图:</p>
<p><img src="/img/bVbtrk5?w=1014&h=715" alt="图片描述" title="图片描述"></p>
<p>▍3.2 部署情况</p>
<p>为了将线上 MySQL 中的 Hive 元数据逐步分布到多套 MySQL 环境存储,需要部署一套新的 MySQL 环境(对应新的 Hive Metastore环境)。</p>
<p>经过内部压力测试,我们得出结论,一个 waggle_dance 实例可以对接于一套 Hive Metastore 环境。考虑 Hive Metastore 环境横向扩展及保证服务的稳定性,部署了一套 waggle_dance 集群由 LVS+4 个 waggle_dance 实例组成。</p>
<p>后续会将已有 MySQL 中存储的 Hive 库,表元数据信息逐步迁移到新的 MySQL 环境,为了迁移过程中减少对用户使用的影响,未来还需要开发 waggle-dance 按表路由等功能。</p>
<p>▍总结</p>
<p>当前方案上线已经稳定运行几个月,新的体系架构会支持横向扩展多套 MySQL 环境,从根本上解决由于一套 MySQL 环境带来的性能及服务稳定问题。</p>
<p>▍END<br>原文来源 / <a href="https://link.segmentfault.com/?enc=CQiZbjHkfg3FhFza3qpNfw%3D%3D.MFkgwb2B7QEs6B37wgVdaSn1n5Xt8ucC8rhOqa2c%2BAs1Yt7pJSpFPBkpCWtVlChE" rel="nofollow">滴滴云(didi-cloud)</a><br>转载请至 / <a href="https://link.segmentfault.com/?enc=NVgBeFusLN3p0%2Ff0sOP%2Bfg%3D%3D.F4xiviBL1tsDiN8XtpQvYhx2ZQUv48BFOuWEJNwilsMNa%2FOhzqbLOwsyHOyxPaPipSAA64Bo4kVzYf%2FDeL03MPxZyhwI9uDv9f%2FPZYxY0Vmh2BoJAFsdV7%2BKA3EE0wqp" rel="nofollow">转载合作入口</a></p>
<p><img src="/img/bVbtdCB?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrmI?w=778&h=308" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
滴滴开源 Rdebug:基于真实流量的研发、调试、测试利器
https://segmentfault.com/a/1190000019370426
2019-06-03T11:34:52+08:00
2019-06-03T11:34:52+08:00
滴滴技术
https://segmentfault.com/u/didijishu
5
<p><strong>出品 | 滴滴技术</strong><br><img src="/img/bVbtrep?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>前言:近日,滴滴在 GitHub 上开源后端研发、调试、测试的实用工具 Rdebug,全称 Real Debugger,中文称作真 · Debugger 。使用真实的线上流量进行线下回放测试,提升研发效率、保障代码质量,进而减少事故。一起来具体了解吧。</p>
<h3>▍背景</h3>
<p>随着微服务架构的普及和应用,一个复杂的单体服务通常会被拆分成多个小而美的微服务。在享受微服务带来便利的同时,也要接受因为微服务改造带来的问题:需要维护的服务数变多、服务之间 RPC 调用次数增加。</p>
<p>这就造成线下环境维护成本大大增加,其次线下环境涉及到的部门较多,维护一个长期稳定的线下环境也是一个挑战;业务快速发展、需求不断迭代,手写单测又因复杂的业务逻辑以及复杂的服务调用需要 mock 多个下游服务,导致手写和维护单测成本特别的高;手动构造数据,又不够全面真实。以上问题都严重影响 RD 的研发效率,并且增加线上产生事故的隐患。</p>
<p><img src="/img/bVbtreL?w=1920&h=1080" alt="图片描述" title="图片描述"></p>
<p>RD 迫切需要一个只需在本地部署代码、不用搭建下游依赖、使用真实数据,进行快速开发、调试、测试的解决方案。Rdebug 基于流量录制、流量回放的思路,能够巧妙的实现上述方案。</p>
<h2>▍宗旨</h2>
<p>提升研发效率、降低测试成本、缩短产品研发周期,保障代码质量、减少线上事故。</p>
<h3>▍使用全景图</h3>
<p><img src="/img/bVbtre7?w=1080&h=632" alt="图片描述" title="图片描述"></p>
<h3>▍全新的研发体验</h3>
<p>只需部署模块代码,无需搭建下游服务;<br>在 macOS 本地回放,开发、调试、测试无需登录远程服务器;<br>流量录制支持常用协议,FastCGI、HTTP、Redis、Thrift、MySQL 等;<br>回放速度快,单次回放秒级别。</p>
<h3>▍录路径重定向</h3>
<p>为了方便 RD 在本地开发、测试,Rdebug 支持路径重定向。</p>
<p>当线上部署路径和本地代码路径不一致时,当代码中存在大量线上路径硬编码时,无需入侵式修改代码,只需要简单的配置即可实现路径重定向。</p>
<p>即代码可以存放在任何路径下回放。</p>
<h3>▍时间偏移</h3>
<p>流量回放时会自动把时间偏移到流量录制的时间点。</p>
<p>在代码中获取时间时,会获得录制时间点之后的时间。所以,当业务接口对时间敏感时,也无需担心。</p>
<h3>▍文件 Mock</h3>
<p>流量回放支持文件 Mock,指定文件路径和 Mock 的内容,即可快速实现文件 Mock。<br>结合录制上报功能,在线上上报配置读取,在线下使用文件Mock实现配置“重现”。</p>
<h3>▍Elastic 搜索</h3>
<p>对存储在 Elastic 中的流量,支持 URI、输入输出关键词、下游调用等多维度搜索。<br>回放支持指定文件,也支持上述搜索回放,使用体验更佳。</p>
<h3>▍Xdebug 调试</h3>
<p>最高效的功能是 Xdebug 联动调试,通过对代码设置断点即可使用线上流量进行调试。通过这种方式,可以用来研究代码、排查问题、查看下游接口响应格式及数据等,是一个开发调试利器。</p>
<p><img src="/img/bVbtrfU?w=1080&h=979" alt="图片描述" title="图片描述"></p>
<h3>▍丰富的报告</h3>
<p>回放报告,汇总线上线下的输入、输出、结果对比,一目了然。</p>
<p><img src="/img/bVbtrgc?w=1080&h=567" alt="图片描述" title="图片描述"></p>
<p>下游调用报告,会列举出所有的下游调用,包括协议、请求内容、匹配上的响应以及相识度。通过不同的背景颜色,标记出完全匹配的流量、存在噪点的调用、缺失的调用、新增的调用等。</p>
<p><img src="/img/bVbtrgC?w=1080&h=631" alt="图片描述" title="图片描述"></p>
<p>结合 Xdebug 生成覆盖率报告,能够清楚的看到哪些代码被执行、哪些代码未被执行以及接口的覆盖率情况。</p>
<p><img src="/img/bVbtrgL?w=1080&h=569" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrhp?w=2283&h=65" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrhv?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p>有关安装、使用过程以及常见问题解答,请查看以下链接:<br>GitHub:<a href="https://link.segmentfault.com/?enc=96KueZYo1EkT%2BPMR1ewAQw%3D%3D.yUWoB0Si1JK5QLIOMZXg%2BNXLWG5s8cY2Q0Nl6CMaiGg%3D" rel="nofollow">https://github.com/didi/rdebug</a><br>Wiki:<a href="https://link.segmentfault.com/?enc=ebyO1pPikRC2K%2FdlqBGl2A%3D%3D.m3N%2Fu%2B6by0pKCxXbs7h5GaMA5JSheyIte4g1H0YkGP4WVCG%2FPQ2PXsG3cRwpTXaD" rel="nofollow">https://github.com/didi/rdebu...</a><br>Documentation:<a href="https://link.segmentfault.com/?enc=I2iFERmX9AXYh%2BYzivJMHg%3D%3D.9P%2ByThLaIaLkf0kl6mymjkRuYP7Tjd%2BRTJNM%2FYTx84J1imSa39Voqc39Y7hHTKMqNezQ7AEobuge536p%2BwCf%2FQ%3D%3D" rel="nofollow">https://github.com/didi/rdebu...</a></p>
<p>同时欢迎加入「Rdebug 用户交流群」<br>请在滴滴技术公众号后台回复「Rdebug」即可加入</p>
<p><img src="/img/bVbtrhL?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbtrhO?w=1920&h=908" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
滴滴陶文:我眼中的技术深度
https://segmentfault.com/a/1190000019318071
2019-05-28T16:16:12+08:00
2019-05-28T16:16:12+08:00
滴滴技术
https://segmentfault.com/u/didijishu
5
<p><strong>出品 | 滴滴技术</strong><br><strong>作者 | 陶文</strong></p>
<p><img src="/img/bVbs78V?w=1280&h=544" alt="图片描述" title="图片描述"><br>前言:本文来自陶文老师在内部发布的话题分享,引发了技术桔们的热议,桔妹在此与大家分享该话题,期待大家看完陶老师的解读,在文末分享自己对技术深度的看法。</p>
<p>经常有同学被挑战工作没有技术深度。不少日常写业务的同学会顿时紧张起来。开始在脑袋里找各种证据证明自己干过的啥事情“有难度的”。</p>
<p>其实这个问题如果准备一下,就不至于慌张了。在我看来,技术深度可以从下面这张图推导而来:</p>
<p><img src="/img/bVbs79e?w=1412&h=581" alt="图片描述" title="图片描述"></p>
<p>技术同学的主要工作是构建一个可运行的 solution 去解决用户的一个 problem。以这个为主题,有两件工作:</p>
<p>运营维护这个 solution,持续去解决 problem。</p>
<p>洞察到 problem 本身的变化,或者有更好的 solution。然后把现有的solution迁移成一个新的 solution 去更好的解决 problem。</p>
<p>技术深度就体现在“更好”地完成这两项工作上,也就是一个优化问题:</p>
<p>▍对运营维护工作而言</p>
<p>降低运营的人工成本:例如自动化代替人工。</p>
<p>降低运营的其他成本:例如更少的机器投入,例如稳定性和安全建设减少风险。</p>
<p>▍对研发工作而言</p>
<p>对 new problem 或者 new solution 的洞察力:数据分析,市场调研,新技术跟进等。提升 solution 对用户的吸引力。新体验-旧体验-迁移成本。</p>
<p>短期敏捷性:因为对 api 很熟悉,能够快速rush出一个版本来的能力。因为对环境很熟悉,可以快速定位 bug 的能力等。</p>
<p>长期敏捷性:架构设计,复杂度管理等。</p>
<p>提供独特 solution 的能力:比如说自动驾驶等科技。从 0 到 1 的过程可以最大化对用户的吸引力,因为很少人提供竞争 solution。</p>
<p>▍每一项优化工作,都可以做得很深</p>
<p>比如你可以投入大量时间学习数据库原理,优化索引检索的效率,从而降低运营的其他成本。</p>
<p>你也可以构建流量录制和回放技术,提供对重构工作的信心保障。从而提高长期敏捷性。</p>
<p>你也可以打磨对产品的洞察力。精通数据分析,倾听用户,对产品的未来演进方向提供自己的洞察。</p>
<p>你也可以锻炼自己快速 debug 的能力,可以在 crash 之后快速用各种工具找到性能瓶颈。这个算是短期敏捷性上的能力。主要是考验对环境和生态是否熟悉。所谓经验活。</p>
<p>其实评委在问你技术深度的时候,并不是问你技术栈的深度(比如是否从像素渲染到硅的提纯都了然于胸),真正在问的是你的竞争力在哪里。</p>
<p>▍你需要想清楚两点</p>
<p>为什么在这个点上,我做过的工作证明了比其他同事要更强。</p>
<p>为什么这个能力是当前公司需要的,也就是所谓的收益。你能手写汇编构造 GUI,但是公司不需要也是没有用的“技术深度”。</p>
<p>希望下次你被问到技术深度问题的时候,能够从容回答。</p>
<p><img src="/img/bVbtdCB?w=929&h=150" alt="图片描述" title="图片描述"></p>
<p><strong>陶文:滴滴 | 首席工程师</strong><br>在滴滴参与过基础架构,核心出行平台重构,业务中台建设等工作,目前在从事平台治理和客服系统,致力于减少大家出行中遇到的不美好。在加入滴滴之前,从事过十余年敏捷咨询,测试开发,运维平台等多个领域的工作。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
资讯 | 滴滴CTO张博:让AI像电力一样服务大众
https://segmentfault.com/a/1190000019252424
2019-05-21T20:04:47+08:00
2019-05-21T20:04:47+08:00
滴滴技术
https://segmentfault.com/u/didijishu
2
<p>出品 | 滴滴技术<br><img src="/img/bVbsWAB?w=2350&h=1000" alt="图片描述" title="图片描述"></p>
<p>前言:3月18日,知名华裔 AI 科学家李飞飞在斯坦福发起成立“以人为本 AI 研究院”(HAI),在成立大会上,滴滴 CTO 张博作为唯一产业界代表发言表示:让 AI 像电力一样服务大众。</p>
<p>网络络购物越来越个性化,打车 App 可以预测目的地,“机器医生”会用大数据进行远程诊断......随着人工智能服务进入寻常百姓生活,公众也越来越关心:AI 接管生活方式,会否带来新的社会问题?怎样做到一方面有效监管,一方面继续推动科技的进步和创新的向前?</p>
<p>针对这些问题, 知名华裔 AI 科学家李飞飞近日在斯坦福大学发起“以人为本 AI 研究院”( HAI ),引起了业界的极大关注。在3月18日的成立大会上,来自斯坦福、微软、谷歌和滴滴的嘉宾就 AI 与人类的互补与竞争进行了深度探讨,展开了一场学术研究和产业实践的有趣对话。</p>
<p><img src="/img/bVbsWAI?w=640&h=421" alt="图片描述" title="图片描述"></p>
<p>△滴滴 CTO 张博表示 AI 相关的学界需要了解社会公众的需求</p>
<p>参加讨论的世界顶尖学者对 AI 的前景继续表示出极大的乐观。斯坦福大学生物工程教授 Altman 和卡内基梅隆大学计算机科学教授 Cassell 介绍说,在医疗和地质科学领域,AI 近年来加速发展,大大扩展了人类的认知边界,让医生和地震预防专家不仅能“知其然”,更走向了“知其所以然”。</p>
<p>同时,与会者也承认 AI 的可持续发展还面临很多科技之外的监管和公众认知挑战。谷歌“人+ AI ”研究负责人 Viégas 指出,机器学习注重准确性,但应用于不同的领域时,光有准确性是不够的,还需要有可解释性;不仅专家可以懂,最终的用户也可以理解并认可。“对于普通用户而言,AI 强大的算法和数学前面有一道墙,我们需要一个可以穿透这道墙的工具,让技术变得可以解释。”</p>
<p>作为这场对话中唯一的产业界代表,滴滴 CTO 张博特别关注 AI 和人类价值在实践中的融合。张博表示,滴滴站在人工智能大规模应用的最前沿,经历试错和成长的艰难过程,对人工智能具有最大的信心,也对其社会应用有最深的敬畏心。</p>
<p>“ AI 像电一样,是一种新的生产力,他能够增强人类做决策的能力,尤其是在数据丰富的领域。AI 也有短板,它还不够灵活,还不能理解人类的感情、文化,还有很长一段路要走。但它的确已经在切实增强人类处理问题的能力。”</p>
<p>张博举例说,滴滴有一万多名客服,每天接到 200 万次用户咨询或者求助,滴滴客服不仅需要给用户准确的回复,同时需要安抚焦躁的情绪。目前,滴滴人工智能实验室( AI Labs )正和客服团队紧密合作,用语音识别、NLP、知识图谱等技术开发 AI 智能客服。它可以先用语音了解了用户的需求并为人工客服提供决策信息,然后人工客服再接起电话跟进处理。它还可以学习人工客服的处理方式,从而使机器越来越接近人的复杂决策水平。</p>
<p>张博更加关注的是为 AI 寻找社会共识的支撑。“很快 AI 也会和电力一样成为无处不在的基本资源。AI 是一种强大的生产力,它可以给人类创造价值,同时,如果不当使用,也有可能带来伤害。AI 需要人类做价值观层面的判断与决策。”</p>
<p>他举例说,高峰时段车辆供不应求是全世界大城市的普遍问题。在美国,Uber 选择动态调价,完全依赖价格杠杆来调节供需。“但是在中国和很多新兴市场,我们提供的是普惠、廉宜的基本叫车服务,完全靠涨价来遏制需求是不是正确的呢?”</p>
<p>经过内部和外部的讨论,滴滴决定在快车这个基本服务上采取排队算法,而在专车等高端产品上采取动态调价,这样来平衡公平和效率的需求。“这不一定是一个完美的答案,但这是一个基于价值观的选择。”</p>
<p>2018年,滴滴就成立了 AI for Social Good(AI 赋能社会)共创平台,与十多所高校、科研机构和社会组织展开合作,在安全、健康、环境、无障碍等几大核心方向进行项目研究,其中包括绘制高清空气质量和噪音污染地图帮助解决环保问题,以及司机 AI 关怀助手等。经历2018年安全事件的挫折,让滴滴对“价值”的反思更加迫切。</p>
<p>“李飞飞教授曾和我谈到,没有爱与关怀的技术是没有意义的。” 张博说,“以前我们想的是要做最大的出行平台,算法效率最高,现在的目标是要做体验最好最有责任感的平台。这就需要形成社会认可的价值共识,并在整个技术和管理架构包括算法体系来贯彻。这些价值判断不是凭空产生,需要企业、政府、研究者和社会公众一起来讨论和共建。”</p>
<p><img src="/img/bVbsWAM?w=640&h=427" alt="图片描述" title="图片描述"></p>
<p>△2018年,滴滴成立 AI 赋能社会共创平台,寻求跨领域的 AI 社会公益应用</p>
<p>在现场问答环节,一名观众指出:“人机协同,不仅是专家企业和机器智能之间的协同,还包括公众和用户的理解。” 在总结发言中, 李飞飞强调,斯坦福HAI研究院成立的目标,正是把人放到技术的中心,推动人工智能研究、教育、政策和实践,并结合AI对人类社会影响的研究,建立起鼓励创新而且造福社群的AI社会治理文化。<br><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
分享 | 滴滴分布式 NoSQL 数据库 Fusion 的演进之路
https://segmentfault.com/a/1190000019241478
2019-05-20T23:32:34+08:00
2019-05-20T23:32:34+08:00
滴滴技术
https://segmentfault.com/u/didijishu
3
<p>出品 | 滴滴技术<br>作者 | 余汶龙</p>
<p><img src="/img/bVbsTFv?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>前言:Fusion 是滴滴自研的分布式 NoSQL 数据库,完全兼容 Redis 协议,支持超大规模数据持久化和高性能读写。在滴滴内部支撑了数百个业务,具有 PB 级别的数据存储量,是使用最广泛的主存储服务之一。在支持滴滴业务高速发展过程中,积累了很多分布式存储领域的经验,孵化了离线到在线的高速数据导入方案、NewSQL 方案、跨机房同步等,一路解决了 Redis 容量限制、 离线数据在线打通、数据库扩展性差、异地多活容灾等问题。</p>
<p>本文来自滴滴的技术专家、Fusion 的负责人余汶龙在 2018 年北京 ArchSummit 全球架构师峰会上的演讲内容,重点介绍了 Fusion 的核心设计以及架构演进过程。</p>
<p><strong>内容框架</strong></p>
<ul>
<li>诞生背景:滴滴业务发展简介</li>
<li>演进过程:如何满足业务需求<p>海量存储<br> FastLoad<br> NewSQL<br> 跨机房多活</p>
</li>
<li>总结 & 展望</li>
</ul>
<p><strong>诞生背景</strong></p>
<p><strong>业务 & 架构演进过程</strong></p>
<p>滴滴出行成立于 2012 年,刚开始创业阶段技术主要靠外包解决,没太多技术沉淀;发展到了 2014 年,乘客司机和单量都有不错的增长,我们开始构建自己的系统架构,这个时候业务对于存储的需求很单纯,简单用用 MySQL 基本能解决我们的问题。</p>
<p>到了 2015 年前后,我们的业务线多了起来,专车快车等开始上线,这个时候我们一方面做了中台系统的重构,另一方面感受到了不小的存储压力,即业务数据量和请求量剧增;到了 2016 年,合并优步前后,日订单量逼近 2000 万,进一步挑战我们的存储系统,于是我们按照不同的业务,对存储进行拆分,因为不同的业务对于存储的需求是不一样的,不同的业务对于吞吐、延迟、容量、数据请求大小等都有不同的需求,分库分表也只是缓兵之计。</p>
<p>如何有效应对这些个性化需求呢?于是在这个时候,我们就开始孵化滴滴自己的 NoSQL 数据库 Fusion 了,用它来丰富我们滴滴的存储生态,为业务提供更多的存储选择。</p>
<p><img src="/img/bVbsTFP?w=1890&h=1048" alt="图片描述" title="图片描述"></p>
<p><strong>Fusion 是什么?</strong></p>
<p>前面我们不断提到 Fusion 的关键字,那么是时候正式介绍下 Fusion。Fusion 是一个兼容 Redis 协议的分布式 NoSQL 数据库。定位介于 Redis 与 MySQL 之间的主存储数据库。怎么理解这个定位呢?也就是性能方面我们向 Redis 看齐,即低延迟;持久化能力方面我们向 MySQL 看齐,即 MySQL 具备的多副本、高可用、ACID 事务,我们都是支持的,同时定位为服务打车订单这样的主流程在线业务。</p>
<p><img src="/img/bVbsTFW?w=1886&h=1054" alt="图片描述" title="图片描述"></p>
<p>它如何实现的呢?大家都知道 Redis 的数据是存放于内存中,虽然性能很好,但是容量极小,且每 GB 存储成本很高(大概是我们 Fusion 的 10 倍以上)。于是我们就基于 SSD 磁盘实现了一套分布式的存储系统,在 SSD 磁盘上实现了 Redis 的数据结构,对外通过 proxy 屏蔽内部细节,让用户像访问 Redis 一样访问 Fusion。当前我们已经支持 StringHashBitmapSetSorted SetList 这些主流的 Redis 数据结构。</p>
<p><strong>演进过程</strong></p>
<p>我们 Fusion 的发展总共经历了 4 个阶段,分别解决了 4 类业务问题,我们接下来重点看下具体过程。</p>
<p><strong>海量存储</strong></p>
<p>首先来看如何解决海量存储的问题。</p>
<p><img src="/img/bVbsTF3?w=1888&h=1056" alt="图片描述" title="图片描述"></p>
<p>Redis 是一款非常优秀的内存数据库,但它也有这样一些已知问题存在:容量受限于内存、扩容迁移和大 key 过期、删除过程是阻塞的、宕机恢复慢等问题。我们 Fusion 设计之初,就避免了这些问题。具体是如何实现的呢?我们从需求分析出发。</p>
<p><strong>需求分析</strong></p>
<p>Fusion 诞生初期,主要解决 2 个业务需求:</p>
<p><strong>一是滴滴的历史订单</strong>,按照前面提到的每日千万级别订单量,很快就能达到几百亿的订单,这么庞大的数据量,存 MySQL 显然是不够灵活的,修改字段、修改索引都比较困难,存 Redis 就更加不可能,因此他们有新型存储的需求;</p>
<p><strong>二是地图团队的司机行程轨迹</strong>,每产生一条打车订单就会产生一条司机行程轨迹,每一条行程轨迹由多个点组成,行程越长轨迹数据越大,这是一个比历史订单的数据量还要大的业务,存储的困难可想而知。</p>
<p><img src="/img/bVbsTGc?w=1894&h=1058" alt="图片描述" title="图片描述"></p>
<p>因此,我们对上述两个业务的需求做了提炼和优先级排定:</p>
<ol>
<li>刚需是海量存储。</li>
<li>具备基本的在线故障处理能力。</li>
<li>稳定性很重要!</li>
<li>性能要足够好,以司机行程轨迹为例,每天 300 亿级别写入,他们对性能的追求当然是越高越好。</li>
<li>接入要求简单,这里我们选择了 Redis 协议。</li>
<li>打通其他存储系统。</li>
</ol>
<p>满足了这些需求后,就诞生了存储系统 Fusion 的雏形。</p>
<p><strong>架构设计</strong></p>
<p><strong>软件结构</strong></p>
<p>下图左边是数据流部分,从下往上看,即 Fusion 是构建在 SSD 磁盘上的存储服务,我们引用优秀的存储引擎 RocksDB 来做磁盘 IO 操作,然后在磁盘之上,我们增加一层 cache 来提升性能,然后封装一层网络框架并支持 Redis RPC,就实现了单机版本的 Fusion 存储节点,然后在单机的基础上加上我们的集群路由管理,Fusion 的集群就搭建好了,当然对外提供服务的时候,还有一层负载均衡。</p>
<p>下图右边是控制流部分,即我们在 SaltStack 平台基础上,构建了用户系统、运维系统、统计、监控、计费等系统,方便用户以及运维人员使用。</p>
<p><img src="/img/bVbsTGG?w=1878&h=1056" alt="图片描述" title="图片描述"></p>
<p><strong>集群架构</strong></p>
<p>集群架构上,我们采用 hash 分片的方式来做数据 sharding。从上往下看,用户通过 Redis 协议的客户端(jedis、redigo、hiredis 等)就可以访问 Fusion,首先会经过 VIP 做负载均衡,然后转发到具体 proxy,再由 proxy 转发数据到后端 Fusion 的数据节点。<br>proxy 到后端数据节点的转发,是根据请求的 key 计算 hash 值,然后对 slot 分片数取余,得到一个固定的 slotid,每个 slotid 会固定的映射到一个存储节点,以此解决数据路由问题。</p>
<p>此外,我们还做了存储生态的打通。支持 Hadoop、MySQL、Redis 的数据同步到 Fusion,也支持 Fusion 数据同步到 MQ,供下游消费。<br><img src="/img/bVbsTGJ?w=1894&h=1060" alt="图片描述" title="图片描述"></p>
<p><strong>小结</strong></p>
<p>接下来就对 Fusion 做个小结,拿 Redis 来做个简单对比。</p>
<p><img src="/img/bVbsTGM?w=1872&h=1054" alt="图片描述" title="图片描述"></p>
<p><strong>FastLoad</strong></p>
<p>我们演进过程中,解决的第二个问题是,离线数据到在线系统的快速打通。因此我们做了一个叫 FastLoad 的系统。</p>
<p><img src="/img/bVbsTGW?w=1884&h=1052" alt="图片描述" title="图片描述"></p>
<p><strong>需求分析</strong></p>
<p>首先,FastLoad 诞生初期主要支持两个业务:标签平台和特征平台。标签平台是指对每个乘客和司机,都打上 N 个标签,然后后续的打车流程会依赖这部分标签,比如优惠券的发放;然后特征平台呢,会收集创建各类特征,对每个对象用某个特征库做一次判断,即可确定某种行为。接下来我们对需求进行提取。</p>
<ol>
<li>高性能。由于这部分数据是在离线计算平台 Hadoop 上加工完成的,业务很容易想到就近存放在 Hive 上,但 Hive<br> 的查询性能实在不能满足在线查询的高吞吐、低延迟要求。因此对于新的存储系统,他们第一个要求就是性能!</li>
<li>定时更新。像特征数据,一般只需要小时级别甚至天级别的更新,所以业务需要有快捷的定时更新功能。</li>
<li>快速更新。特征数据还有一个特点,就是数据量特别大,以乘客特征为例,动辄上亿条数据,约 TB 级别数据量。这么大的数据量通过 SDK<p>写入肯定是不行的。刚开始业务方也确实是这么玩的,直接通过 Hadoop 任务调用 Redis SDK,然后一条条的写入<br> Fusion,一般是每天凌晨开始写数据,等到早高峰 8 点时大量读取。但是这种方法实践下来,经常导致 Fusion<br> 各类超时,在早高峰打车已经来临 时还在写凌晨的数据,非常影响稳定性。因此第 3 个需求是必须快速更新。</p>
</li>
<li>稳定性。这个是毋容置疑的。</li>
<li>多表隔离。有些业务有很多类特征数据,他们有隔离存储的需求,也有分类更新、分类查找的需求,因此需要多表来支持逻辑到物理的隔离。</li>
</ol>
<p><img src="/img/bVbsTHx?w=1880&h=1056" alt="图片描述" title="图片描述"></p>
<p><strong>架构设计</strong></p>
<p>满足上述需求后,就诞生了我们的 FastLoad 系统。接下来就来看下我们的架构是如何设计的。我们给用户提供两种接入方式:控制台和 OpenAPI。用户通过任一一种方式提交 FastLoad 任务时,都会在我们的 FastLoad 服务器上,创建一个 DTS 任务,该任务会在 Hadoop 配置中心注册一个调度任务(周期性或一次性,由用户决定),然后 FastLoad 服务器根据用户上传的数据存储路径或 Hive 表(我们支持的数据源有:HDFS 上的 JSON 文件和 Hive 结构的数据),按照用户提交的拼 key 方式,我们启动 map/reduce 任务直接构造 Fusion 底层存储在文件系统上的文件 SST,并把它们构造好相互之间的排序,避免重复,构造好后通知 Fusion 存储节点,下载 SST 文件,然后 load 到 Fusion 数据库中。此后,用户就可以通过 Redis-Client 访问我们帮它加载的数据了。</p>
<p><img src="/img/bVbsTHX?w=1884&h=1064" alt="图片描述" title="图片描述"></p>
<p><strong>小结</strong></p>
<p>总结一下我们的 FastLoad 一站式 DTS 平台,有如下优势:</p>
<ol>
<li>减少 N 次网络交互。相比调用 Redis SDK 的方式写入,我们减少非常多的网络交互,传输的是压缩格式文件,节省了网络带宽。</li>
<li>对用户请求 0 影响。我们利用 map/reduce 的计算能力,做了 SST 的全局排序,让 SST 进入 Fusion<br> 的时候,不经由 L0,直接到达最终 level,避免了 LSM 的 compact 影响,因此对用户可以说没有影响。</li>
<li>接入简单,用户 0 感知细节。用户既不需要关心 Hadoop 使用、任务调度,也不需要自己写 Redis SDK<br> 的代码,只需要告诉我们,在什么时间点需要什么样的数据即可!</li>
<li>提供了 OpenAPI,方便用户的自动化流程打通。</li>
<li>提供全量覆盖和增量导入两种方式。</li>
</ol>
<p><img src="/img/bVbsTIb?w=1884&h=1048" alt="图片描述" title="图片描述"></p>
<p><strong>NewSQL</strong></p>
<p>在演进过程的第 3 个阶段,我们主要是针对 MySQL 的。大家都知道 MySQL 的扩展性比较差,面对百亿级存储,有几个问题,一个是数据存不下,一个是扩展不灵活,比如修改字段、修改索引等。接着就来讨论下,我们是如何解决这类问题的。<br><img src="/img/bVbsTId?w=1878&h=1058" alt="图片描述" title="图片描述"></p>
<p><strong>需求分析</strong></p>
<p>同样的,我们先来分析下业务的需求是什么?简单理解下,我们认为有 3 点刚需:</p>
<ol>
<li>轻松改字段。即需要足够的扩展性。</li>
<li>存储不限量。即需要一个容量尽可能大的存储。</li>
<li>省成本。既然需要存 MySQL 都存不下的数据,那么成本一定要考虑清楚。</li>
<li>至于事务、稳定性、高性能、二级索引,我们认为都是基本需求。</li>
</ol>
<p><img src="/img/bVbsTIh?w=1882&h=1054" alt="图片描述" title="图片描述"></p>
<p><strong>背景问题</strong></p>
<p>如何实现 shema 到 key/value 的转换?</p>
<p>前面的介绍我们知道,Fusion 是支持 Redis 协议的,那么 schema 转换成 key/value,就可以用 Redis 的 hash 结构来实现,下图我们就以 student 表为例,转换了 2 行数据。<br><img src="/img/bVbsTIj?w=1080&h=606" alt="图片描述" title="图片描述"></p>
<p><strong>如何做主键查询呢?</strong></p>
<p>下面的图片给出了一个例子,即查询 ID 为 1 的学生的全部信息或年龄。</p>
<p><img src="/img/bVbsTIk?w=1888&h=1054" alt="图片描述" title="图片描述"></p>
<p><strong>如何实现二级索引呢?</strong></p>
<p>我们还是以 student 表为例,分别构建如下 agesex 索引,其编码规则如下可见。</p>
<p><img src="/img/bVbsTIn?w=1888&h=1062" alt="图片描述" title="图片描述"></p>
<p><strong>如何做非主键查询和范围查询呢?</strong></p>
<p>在上图构建好索引后,就很容易实现下面的两个例子,即查询年龄在某个范围的学生,和查询某种性别的所有学生。</p>
<p><img src="/img/bVbsTIw?w=1880&h=1058" alt="图片描述" title="图片描述"></p>
<p><strong>背景问题</strong></p>
<p>架构设计上分成接入层和数据存储层,在接入层(DISE)我们提供控制台来管理用户的字段,用户可以在这里定义自己的 schema、字段、索引,并做相应的修改。然后用户通过我们提供的类 SQL 的 SDK 接入,由我们的 SchemaServer 做 schema 转换,接着插入数据到存储层。然后数据存储层吐出 binlog,由 IndexServer 异步消费 binlog 并构建索引。查询时候,用户的请求经由 SDK 到达 SchemaServer,SchemaServer 先查询索引服务器,拿到对应的主键信息,然后根据命中的主键查询详细信息,最后返回给用户。</p>
<p><img src="/img/bVbsTIB?w=1596&h=894" alt="图片描述" title="图片描述"></p>
<p><strong>小结</strong></p>
<p>NewSQL 解决的问题是针对 MySQL 的特殊场景的,我们就拿 MySQL 来跟 Fusion 做个对比,可以看到 Fusion 只是在部分场景上解决了 MySQL 的容量限制以及扩展问题,但还是有很多场景并不能支持的。</p>
<p><img src="/img/bVbsTIG?w=1882&h=1056" alt="图片描述" title="图片描述"></p>
<p><strong>跨机房多活建设</strong></p>
<p>最后一个演进我们讲的是如何支持业务的跨机房容灾问题。</p>
<p><img src="/img/bVbsTIJ?w=1880&h=1054" alt="图片描述" title="图片描述"></p>
<p><strong>背景介绍</strong></p>
<p>滴滴多活的业务架构如下图,可以看到用户层接入层和业务层都是无状态的,因此如图中的白色虚线所描述的,他们的请求可以在两个机房间来回路由,而不影响业务请求的正确性。那么是如何做到这一点的呢?必然得有一个地方维护着状态的一致性,才能让业务自由切换。因此跨机房多活容灾最复杂的部分就在底层数据同步层,这里的数据同步涉及到很多中间件,我们这里只关心 Fusion 的跨机房多活。</p>
<p><img src="/img/bVbsTJ1?w=1886&h=1050" alt="图片描述" title="图片描述"></p>
<p><strong>架构设计</strong></p>
<p>下图是 Fusion 的跨机房同步架构,不依赖任何外部中间件,也不依赖内部 proxy。当用户数据通过 A 机房写入时,落地到某个存储节点上,该存储节点会 cache 一份对端节点的路由表,并异步的将刚才写入的数据转发到对端集群。</p>
<p>我们这里的转发采用了两个异步特性:1. 跟用户写入主流程完全异步,即不影响用户正常请求;2. A 机房到 B 机房的数据同步,采用了异步、批量、应答的方式高效同步。既保证了用户请求主机房的吞吐和延迟,也大幅降低了备机房数据读取的延迟。</p>
<p><img src="/img/bVbsTJ6?w=1884&h=1056" alt="图片描述" title="图片描述"></p>
<p><strong>小结</strong></p>
<p>到此总结下我们的多活方案:</p>
<ol>
<li>异步数据复制。在追求性能的同时,放弃了一段时间的不一致。如果在数据未达成一致的时候,主机房宕机,备机房的数据将缺失,但这个缺失不会是永久,等到主机房恢复后,我们会把这部分数据自动补齐到备机房,这个过程我们会根据时间戳去重。</li>
<li>自适应感知集群状态变更,比如切主、扩容等。在运行过程中,两个机房的集群难免会发生各类路由变化,我们在设计时考虑到了这一点,针对路由变化,我们会及时更新路由表,以把数据同步到正确的节点上。</li>
<li>数据可靠同步。我们的数据同步是依赖滑动窗口的应答机制,因此实现了一种可靠的数据同步。</li>
<li>支持双写,解决秒级冲突。由第一点提到,在某些场景是存在双写的,如果双写的是不同 key,自然不需要解决冲突,如果双写的是针对同一个<br> key,那么我们会根据时间戳做冲突检测。</li>
<li>自动数据补偿。也就是在发生主机房宕机后,写入备机房的增量数据,可以自动的补偿到主机房;原先滞留在主机房的数据,在主机房恢复后,也可以补偿到备机房。即可以达到最终一致性。</li>
</ol>
<p><img src="/img/bVbsTKp?w=1882&h=1054" alt="图片描述" title="图片描述"></p>
<p><strong>总结&展望</strong></p>
<p><strong>总结</strong></p>
<p>在伴随滴滴业务发展的过程中,Fusion 经历了 4 个发展阶段,我们坚持”好东西是用出来“,因此在每个阶段,都尽量避免”过度设计“,只解决特定的业务问题。这给了我们很多认真打磨产品的时间和精力,让我们赢得了用户口碑。</p>
<p><img src="/img/bVbsTKs?w=1892&h=1056" alt="图片描述" title="图片描述"></p>
<p><strong>展望</strong></p>
<p>通过前面的分享我们知道,Fusion 虽然能做的事情很多,但不能做的事情更多,所以我们的目标是持续发展持续演进,把解决业务问题当做己任。未来我们将往分布式数据库方向前进,解决更多的业务问题。</p>
<p><img src="/img/bVbsTKt?w=1880&h=1050" alt="图片描述" title="图片描述"></p>
<p><strong>END</strong></p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
Chameleon 跨端框架——一个理想主义团队的开源作品
https://segmentfault.com/a/1190000019235310
2019-05-20T14:30:54+08:00
2019-05-20T14:30:54+08:00
滴滴技术
https://segmentfault.com/u/didijishu
2
<p>出品 | 滴滴技术<br>作者 | 张楠</p>
<ul>
<li>背景</li>
<li>解决方案</li>
<li>原理</li>
<li>久经考验</li>
<li>生产应用举例</li>
<li>易用性好</li>
<li>多态协议</li>
<li>学习成本低</li>
<li>渐进式接入</li>
<li>业内对比</li>
<li>后期规划</li>
<li>理想主义</li>
</ul>
<p>历经近20个月打磨,滴滴跨端方案chameleon终于开源了<a href="https://link.segmentfault.com/?enc=lj6FiQvPwFr0G%2Fu9bMToyg%3D%3D.SvU8P%2F85xtHtkE1eRB2THQ%2FMmhXJD33GE5owBV%2Br10hQE75GPnzsOZsgKrVUHMt2" rel="nofollow">https://github.com/didi/chame...</a> 真正专注于一套代码运行多端。</p>
<p><strong>一、背景</strong></p>
<p>微信月活10亿月活(超过网民数量,用户多个账号?)、支付宝4亿月活、百度3.3亿月活;2018 Q3中国Android手机占智能手机市场超过80%;无论BAT还是Android快应用都是中国互联网用户的真正用户入口,作为小型互联网公司都希望能搭上小程序的风口,从而蹭一波流量。</p>
<p>计算机技术历史发展告诉我们,每一种新技术出现都会经历"各自为政"的阶段,小程序技术也不例外。微信小程序作为首创者,虽然其他小程序都在技术实现原理、接口设计刻意模仿,但是作为一线开发者面对同样的应用实现往往需要重复开发、测试,从前1单位的工作量变成了N单位的工作量。</p>
<p>滴滴的研发工程师是其中最显著的"受害者"之一,滴滴出行在微信钱包、支付宝、Android快应用都有相关入口,用户流量占比不低。</p>
<p>各类小程序已经能覆盖中国所有网民,从Facebook在2013年开源react,这个项目本身越滚越大。从最早的WebUI引擎衍生的ReactNative项目,目标更是宏伟。</p>
<p>Vue.js于2014年左右发布,逆流而上占据了大量用户群体,2016年阿里巴巴也基于vue发布了weex项目,可以用vue编写NativeAPP。</p>
<p>Google在2018年末正式发布了面向未来的跨Andoid、IOS端Flutter1.0.0,作为面向未来的跨端框架,前景一片光明。</p>
<p><strong>二、解决方案</strong></p>
<p>虽然不同各端框架环境千变万化,无论各类小程序、Weex、React-Native、Flutter、快应用,它们万变不离其宗的是MVVM架构设计思想。Chameleon希望既能用一套代码完成所有端需求,将相同的业务逻辑完成收敛到同一层系统里面,又不会因为项目的抽象一致导致可维护性变差。</p>
<p><img src="/img/bVbsRXh?w=1080&h=954" alt="图片描述" title="图片描述"></p>
<p>让MVVM跨端环境大统一:以各个跨端技术(Weex、React-Native、WebView/浏览器、Flutter)和产品业务(微信小程序、快应用、支付宝小程序、百度智能小程序、今日头条小程序、其他各类小程序)的共同技术特点——MVVM架构设计, 以统一MVVM跨端架构平台为目标的程序语言框架Chameleon(任意使用MVVM架构设计的终端,都能以Chameleon开发并运行)。</p>
<p><strong>View:</strong></p>
<p>ChameleonSDK包括各类小程序、web端、客户端(React-Native、Weex、Flutter),目前支持微信小程序、Web、Weex三类,后续支持更多MVVM为标准的端。</p>
<p><strong>View Model:</strong></p>
<p>CML(Chameleon MarkupLanguage)是框架设计的一套标签语言,结合基础组件、事件系统、数据绑定,可以构建出页面的结构。同时为了降低学习成本支持类VueTemplate。</p>
<p><strong>三、原理</strong></p>
<p><img src="/img/bVbsRXF?w=1080&h=505" alt="图片描述" title="图片描述"></p>
<p><strong>四、久经考验</strong></p>
<p>2017年时微信小程序发布,滴滴作为白名单用户首先开始尝试接入。这时候我们专门成立了一个1、2人的小项目组,完成一个名为MPV的项目,一期目标是“不影响用户发挥,不依赖框架方的原则性实现一套代码运行web和微信小程序”。MPV研发完成后,在多个项目实践中,确实完成了超过90%代码重用,总体上开发效率和测试效率都有了明显提升,同时暴露出更多问题,在MPV的实践积累下,有了一定的底气和把握,后续的规划更加明确。</p>
<ul>
<li>可维护性问题,没有隔离公用代码和各端差异代码。</li>
<li>方向选择错误,MPV使用了小程序语法标准(小程序的生命周期、API接口等),导致用户使用上无法清晰理解使用规范。</li>
<li>各端周边小型差异点太多。</li>
<li>模板DSL语法不规范。</li>
<li>两端界面效果不一致。</li>
<li>多端调试成本高。</li>
<li>工程化建设落后。</li>
<li>不能直接使用各端已有生态组件,即缺乏标准规范接入某个端已有开源组件。</li>
</ul>
<p>2018年4月我们把跨端项目规模进一步扩大,跨N端的解决方案命名为Chameleon/kmiln/,简写CML,中文名卡梅龙;中文意思变色龙,意味着就像变 色龙一样能适应不同环境的跨端整体解决方案。</p>
<p>Chameleon在MPV的实践积累下,不仅解决了遇到的各种可维护性问题,后续的规划更加明确,目标真正专注于让一套代码运行多端,提供标准的MVV M模式统一各类终端。</p>
<p>经过一年数十位前端开发人员在上百页面中的实践经验积累,在本周正式开源:<a href="https://link.segmentfault.com/?enc=hbvsKhm6ggjBTAlBDbomlg%3D%3D.H0%2B723Hm1COiVCC7CuT51gLBHLrNUPs0Ad2%2FoZZ%2FCC3UehGcjV5zdQH4hB5pFYTx" rel="nofollow">https://github.com/didi/chame...</a>。</p>
<p><strong>五、生产应用举例</strong></p>
<p><img src="/img/bVbsRXW?w=784&h=817" alt="图片描述" title="图片描述"></p>
<p><strong>六、易用性好</strong></p>
<p>一套代码运行多端理念,被人挑战最多的如何保证易用性。</p>
<ul>
<li>一致性,多端实现效果一致。</li>
<li>简洁性,各端开发定制化空间大,且公用代码不会混杂某端代码。</li>
<li>性能好,不能增加产出文件包大小。</li>
<li>开发快,整体开发流程要高效。</li>
</ul>
<p><img src="/img/bVbsRYh?w=1376&h=886" alt="图片描述" title="图片描述"></p>
<p><strong>七、多态协议</strong></p>
<p>多端合并后各端差异化实现在所难免,一开始是差异化代码和业务逻辑混杂在一起。这就尴尬了,如果你觉得以上不复杂,假设有4、5个端呢,业务逻辑掺杂跨端逻辑,产品逻辑别打断,可读性差,需求变更,牵一发动全身,每个端都要测试,跨端代码效率变得适得其反。</p>
<p>下图各端差异化代码也何物逻辑混合在一起</p>
<p><img src="/img/bVbsRYw?w=803&h=388" alt="图片描述" title="图片描述"></p>
<p>多态协议设计的灵感来自于Apache Thrift - 可伸缩的跨语言服务开发框架,本质上跨端也属于跨语言。 它能让Chameleon开发者快速接入各个客户端底层功能或者差异化业务实现,避免可读性差、可维护性差的问题。</p>
<p>多态协议通过定义标准接口(interface),各端模块各自独立实现,编译时和运行时对实现的接口输入输出做检查。</p>
<p>主要2个目标:</p>
<p>保障多端可维护性<br>编译时拆分多端代码<br>当用户按照标准规范扩展个别产品效果多端不一致或特定底层能力多端不一致的的功能时,多态协议可以有效隔离公用代码和各端差异代码,保证”河水不犯井水“。</p>
<p><img src="/img/bVbsRYT?w=806&h=289" alt="图片描述" title="图片描述"></p>
<p>举例:当你像开发一个图表功能组件时,可能用到 echarts :</p>
<ol>
<li>在项目中分别按照web版本 npm install echarts 和微信版本下载相关文件</li>
<li>然后定义一个多态组件 charts</li>
<li>在 charts/charts.interface 定义该组件的输入和输出</li>
<li>分别在 charts/charts.wx.cml 和 charts/charts.web.cml 里面调用微信版本(可使用微信小程序组件文件夹)和web版本(可调用.vue后缀文件)</li>
<li>最后就能在项目中使用该组件</li>
</ol>
<p>产出包里面只包含该组件其中一端的代码;因输入输出的限制,该组件调用上完全一致,不用根据某端做特殊逻辑处理。你可以将该echart多态组件单独放置在一个仓库里面单独维护并发布;其他人无需关系内部细节,直接 npm install echart即可使用。</p>
<p><strong>八、学习成本低</strong></p>
<p>VM层的CML语法是关联视图层和逻辑层的抽象DSL,其有学习成本问题是被热心很多帮助我们的同学提的最多建议,本身其CML学习成本已经非常低,无非是数据双向绑定、事件绑定、组件树、条件语句、循环遍历等等。一开始我们是拒绝的,后来综合考虑之下,还是妥协支持了类vue语法,让开发者更快上手CML。</p>
<p><strong>九、渐进式接入</strong></p>
<p>很多人已经开发小程序了,又不愿意大多阔斧重新改造,也希望使用CML?当然可以,2种方式使用CML:</p>
<p><img src="/img/bVbsR8h?w=672&h=896" alt="图片描述" title="图片描述"></p>
<p><strong>十、业内对比</strong></p>
<p>业内其他框架和我们的目标不一样,我们是希望真正一套代码运行多端,而其他框架无非是“某个小程序语法增强”或者“推广某个框架写小程序 ”,但却是有重合点,列举一下功能对比:<br><img src="/img/bVboFT2?w=1380&h=726" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVboFT1?w=1976&h=666" alt="图片描述" title="图片描述"></p>
<p><strong>十一、后期规划</strong></p>
<p><img src="/img/bVbsR81?w=1332&h=1754" alt="图片描述" title="图片描述"></p>
<p><strong>十二、理想主义</strong></p>
<ol>
<li>我们忍受不了自己的时间浪费在重复劳动上。</li>
<li>要么不做要做就到极致,一套代码运行多端本来就是理想主义,这条路很艰苦,我们却偏执的坚信一定要尽最大努力做出来,作为一个不那么自 信的人,不做到好用是不敢发布出来的。</li>
<li>CML框架各个细节都要做到极致,我们不能容忍有设计上的缺陷,所以常常CML周会上团队成员讨论6个小时直到深夜。</li>
</ol>
<p>快速开始:<a href="https://link.segmentfault.com/?enc=CbqFY9KykW0pH3TNfPm6qw%3D%3D.%2B937cagPzcMP5xvUUHSq20owv6ZUCKxTSgqL%2FJBGbKTi4zsngsZNSx8LbkWoVzRAtzFJe0lVOpwkV9tHmIemVQ%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=qCZG96h6jxEgBJnI4o6OhQ%3D%3D.TXkau5OKq1VakPmWK4VLoA6ubji%2F%2BipS6jjReWeva2r50hkIRzEr0QOjyVAUzNy5azVMB%2BT%2FZ3MFg3PE4sklRw%3D%3D" rel="nofollow">https://cmljs.org/doc/quickst...</a> </p>
<p>常见问题: <a href="https://link.segmentfault.com/?enc=tAIMr1Fe32sEGF%2F7tnU9Pg%3D%3D.Hon8RO2FMw6Jn%2BZQSD8yczFbnpAzsD6zzwE3TGps1v7S8ROga9L%2BoD7GVJGWez0N" rel="nofollow">https://cmljs.org/doc/framewo...</a></p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
Router-Based HDFS Federation 在滴滴大数据的应用
https://segmentfault.com/a/1190000019234423
2019-05-20T13:28:17+08:00
2019-05-20T13:28:17+08:00
滴滴技术
https://segmentfault.com/u/didijishu
2
<p>出品 | 滴滴技术<br>作者 | 费辉</p>
<p><strong>一、背景</strong></p>
<p>HDFS 的 Master/Slave 架构,使得其具有单点瓶颈,即随着业务数据的大规模膨胀,Master 节点在元数据存储与提供服务上都会存在瓶颈。为了克服 HDFS 单点瓶颈存在的扩展性、性能、隔离问题,社区提出了Federation(<a href="https://link.segmentfault.com/?enc=Lol0Hqzz0MBbPNBnyELMOw%3D%3D.KAaVdzHnmx77UmrEeEqHbdHsXFdw7L01Hve1eQKJjknJIYtVWPwkcDsjzr%2F9kizp" rel="nofollow">https://issues.apache.org/jir...</a> )方案来进行解决。</p>
<p>但是使用该方案之后,暴露给客户的问题就是,同一个集群出现了多个命名空间(namespace),客户需要知道读写的数据在哪个命名空间下才可以进行操作。为了解决统一命名空间的问题,社区提出了基于客户端(client-side)的解决方案 ViewFS(<a href="https://link.segmentfault.com/?enc=ujEIJbYOeScVhZ%2B5az5G%2Fg%3D%3D.7mDjnLbL%2FR8cBIGuZzb2Zgx23%2BY8lPIoiuhlyD1M9lS4vfMFMJxE7PjqelgbAhWpHtzus%2BGS4x5GkM4D4oCyKQ%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a> ),该方案会在客户端做好配置,用户目录一对一的挂载到具体的命名空间目录上,滴滴在解决 Federation 问题时使用的就是这个方案。</p>
<p>ViewFS 方案也存在一些问题:<br>对于已经发布出去客户端升级比较困难;<br>对于新增目录需要增加挂载配置,与产品对接,维护起来比较困难。<br>社区在 2.9 和 3.0 版本中发布了一个新的解决统一命名空间问题的方案 Router-Based Federation(<a href="https://link.segmentfault.com/?enc=U%2BMYO%2Ftkkh0RehWLOJRI4A%3D%3D.HBmGFahsxPXQ9WUgHcODtUyif5RD7OV6%2F5Lebtj6DAb11iUrhtw1qVFbS%2BhS3d7WwEMhxRU6QHzJzZsuwWBgiA%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a> ),该方案是基于服务端进行实现的,在升级管理方面比较好维护,滴滴最近引入了该方案,并进行了一些改造。</p>
<p><strong>二、Router-Based Federation 方案介绍</strong></p>
<p>Router-Based Federation 对外提供了 Router 服务,包含在 Federation layer 中,如下图所示。这个 Router 服务将允许用户透明地访问任何子集群,让子集群独立管理自己的 Blockpool。为了实现这些目标,Federation layer 必须将 Block 访问引导至适当的子群集。同时,它具有可扩展性,高可用性和容错性。</p>
<p><img src="/img/bVbsRUq?w=1080&h=354" alt="图片描述" title="图片描述"></p>
<p>Federation layer 包含多个组件。Router 是一个与 Namenode 具有相同接口的组件,根据 State Store 的元数据信息将客户端请求转发给正确的子集群。State Store 组件包含了远程挂载表(具有 ViewFS 特性,但在客户端之间共享)和有关 SubCluster 的负载/空间信息。</p>
<p>下图架构中显示每个子集群增加了 Router(标记为“R”)和逻辑集中式(但物理分布式)的状态存储(State Store),以及每个 SubCluster 的 Namenodes(“NN”)和 Datanodes(“DN”)。这种方法与 YARN Federation(YARN-2915)具有相同的架构。</p>
<p><img src="/img/bVbsRUr?w=1080&h=459" alt="图片描述" title="图片描述"></p>
<p><strong>1、Router 组件</strong></p>
<p>系统中可以有多个的 Router,每个 Router 有两个角色:<br>1)向客户端提供一个全局 Namenode 接口并负责转发请求正确的子群集中的 Active Namenode;<br>2)在 State Store 中维护关于 Namenode 的信息。</p>
<p>Router 在收到客户端请求,根据 mount-table 中的信息查找正确的子集群,然后转发对该集群请求到对应子集群 Active Namenode。在收到 Active Namenode 的响应结果之后,将结果返回给客户端 。 为了提升性能,Router 可以缓存远程挂载表条目和子集群的状态。</p>
<p>对于 Namenode 信息的维护,Router 定期检查一个 Namenode 的状态和向 State Store 报告其高可用性(HA)状态和负载/空间状态。 为了提高 Namenode HA 的性能,Router 使用 State Store 中的高可用性状态信息,以将请求转发到最有可能处于活动状态的 Namenode。</p>
<p><strong>1.1 可用性与容错性</strong></p>
<p>Router 是无状态的,所有 Router 同时提供服务。如果某个 Router 变成不可用,不影响其他任何 Router 提供服务。</p>
<p>客户端配置他们的 DFS HA 客户端(例如 ConfiguredFailoverProvider 或 RequestHedgingProxyProvider)与 Federation 中的所有 Router 配合使用。</p>
<p>为了实现高可用性和灵活性,多个 Router 可以监控相同的 Namenode 并把心跳发送信息到 State Store。 如果 Router 出现故障,这会增加信息的恢复能力。</p>
<p><strong>1.2 Safe Mode</strong></p>
<p>如果 Router 不能连接到 State Store,它可能会错误地提供过期 locations 的访问,让 Federation 进入不一致的状态。</p>
<p>为防止这种情况发生,当 Router 无法连接到 State Store 一段时间后,它会进入安全模式(类似于 Namenode 的 safe mode)。当客户端尝试访问 safe mode 的 Router 时候,会抛出异常,客户端的 Proxy 捕获后,会尝试连接其他的 Router。类似于 Namenode,Router 保持在这个安全模式,直到它确定 State Store 可用为止。</p>
<p>这可以防止 Router 启动时出现不一致。 假定一个 Router 如果在一段时间内没有心跳(例如,心跳间隔的五倍),则它已经死亡或处于安全模式。</p>
<p><strong>1.3 交互接口</strong></p>
<p>为了与用户和管理员进行交互,Router 公开了多个接口。包括 RPC、Admin、WebUI 。</p>
<p>RPC 实现了客户端与 HDFS 交互的最常见接口。 目前仅支持使用普通 MapReduce,Spark 和 Hive ( on Tez,Spark 和MapReduce)。一些高级特性,如快照、加密和分层存储在未来版本实现。 所有未实现的功能都会抛出异常。</p>
<p>Admin 为管理员实现的一个 RPC 接口,包括从子集群获取信息、添加/删除条目到 mout table。也可以通过命令行获取和修改 Federation 信息。WebUI 实现了一个可视化 Federation 状态,模仿了当前的 Namenode UI,除此之外,还包含 mout table,每个子集群的成员信息以及 Router 的状态。</p>
<p><strong>2、State Store 组件</strong></p>
<p>State Store 维护的信息包括:<br>1)子集群的块访问负载,可用磁盘空间,HA 状态等状态;<br>2)文件夹/文件和子集群之间的映射,即远程 Mount Table;<br>3)Router 的状态。State Store 的后端存储是可配置的。 既可以可以存储在文件中,也可以存在 ZooKeeper 中。</p>
<p><strong>2.1 Membership</strong></p>
<p>Membership 反映了 Federation 中的 Namenode 的状态。包括有关子集群的信息,例如存储量和节点数量。Router 定期检测一个或多个 Namenode 的信息。</p>
<p><strong>2.2 Mount Table</strong></p>
<p>管理文件夹和子集群之间的映射。 它与 ViewFS 中的 Mount Table 类似:hdfs://tmp → hdfs://C0-1/tmp /<em> Folder tmp is mapped to folder tmp in subcluster C0-1 </em>/</p>
<p><strong>2.3 Router State</strong></p>
<p>为了跟踪 Router 中 caches 的状态,Router 将其版本信息、状态信息等存储在 State Store 中。</p>
<p><strong>3、未来计划</strong></p>
<p>目前 RBF 只是实现了一些基本 Namenode 接口,有些接口并不支持,HDFS-13655(<a href="https://link.segmentfault.com/?enc=lfg6eE0pg97EWneeur968Q%3D%3D.SpZBvYDSCgLjlx632XkRm11WJkunHVFTdOBTtdaoxpkDEYuIJhYJNX2sSRPKYJRPK8Sj5EIdaUOepbEIp653aw%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a> )中会实现一些不支持的协议接口;当前 RBF 的稳定性也还存在一些问题,HDFS-13891(<a href="https://link.segmentfault.com/?enc=hLjulZI%2BlxROXQWnf9db5Q%3D%3D.jo9Oa5zS3fZMiPbQO%2F506ONrftnwyM9O8gJF24SsCRryFf5enPfr0Xc0HXZqdI1omvERPYd%2FNHcU0ZPudI5HpQ%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a> )会跟踪一些稳定性问题进而解决掉。</p>
<p><strong>三、Router-Based Federation 在滴滴的应用</strong></p>
<p><strong>1、部署情况</strong></p>
<p>社区 Hadoop 在 2.9 和 3.0 中发布了 RBF 这个 Feature,滴滴目前的 Hadoop 版本是 2.7.2,我们的做法是将 branch-2 分支里关于 RBF 的提交都移植到了我们的代码中,做了一些必要的修改工作。</p>
<p>在滴滴的大数据集群中,Federation 拆成了 5 组 Namenode。经过性能测试,我们得出这样的结论:一个 Router 对应服务一组 Namenode 不存在压力,因此我们选择部署 5 个 Router 来服务整个集群。目前 Router-Based Federation 方案在滴滴已经稳定运行 2 月有余。</p>
<p><strong>2、兼容性</strong></p>
<p>直接引入 RBF 在运行 Hive 任务时会出现一些错误,例如 Wrong FS 等等。为此我们将 Hive 客户端代码做了修改,使其兼容 RBF。在 Hive 的元数据存储中,location 信息存储的是带HDFS Schema 的绝对路径信息,在 Hive 代码中处理 move 逻辑时,我们都会将路径做一个 resolve 得到实际的 HDFS 路径,然后再进行处理,这样可以避免该问题的出现。</p>
<p><strong>3、RBF 社区贡献</strong></p>
<p>在实际测试中,我们也发现了 RBF 的一些性能问题和 BUG,包括 Quota 问题、mount-table cache 使用不当问题、mount-table 创建 znode 出现 Null 问题等等。在解决这些问题之后,将 patch 贡献给了社区,大部分被社区接收,具体修复和优化如下:<br>HDFS-13710:<a href="https://link.segmentfault.com/?enc=S%2FVRL5BvFwj59rLRQPN9WQ%3D%3D.0j3AlnUJZgOyHyeiPxRFLY9ZWcC5kNA5E73hTmLPQNPQ2zgZkzLpIqzrYeWYqhtor9RpHi%2Bf7gZZvZwixHBBUw%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13821:<a href="https://link.segmentfault.com/?enc=NQG%2BykdvIatU%2FPXO7rv5fQ%3D%3D.M2JGA4zL%2BrAQsk3eVo0gKgiij3uWRc7nIvWsXf1DZtYjJ0%2BSItemGeHGcFfC1wAez2x3k5GZGQWcsFFQ1vH3iA%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13836:<a href="https://link.segmentfault.com/?enc=mfgZiB5wHXS48P%2FbjVKRUw%3D%3D.XQEpuw7WefnEv5Lm9t4aZQjgB6piShfX5%2Fcqv%2BNU3P%2BOZu%2B6j%2Bv%2BsBt8LNQMkxWZWuNKznWJ0ZdwW3dgYYQOkA%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13844:<a href="https://link.segmentfault.com/?enc=rXjIKwQplVHfhnhLJYGJ3Q%3D%3D.Beg3qMARXKIwdFLD6Ob5Jd7R%2B0999hOHVp3t0rbjHVmoc4kbmI694VcymJBJuBInr5kMZnnx4zBFx11dNdmbYw%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13845:<a href="https://link.segmentfault.com/?enc=t1LNYs%2FlB0dHR5MQq2EF9A%3D%3D.J7J%2BQAx6AAWYUoEPLdOwyk7%2FJVLHw0gR1BWuyPhra4GsIplcEWfrPqgFygH0COJEwP3ncBCFrLra%2B9W2my%2F%2B4g%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13854:<a href="https://link.segmentfault.com/?enc=dgcLDJLoYwp9s%2B3ZDJSszQ%3D%3D.Olaym0jsUcTCVwuPCUW0oyppdra1vyOdSAcgpnkxQajoOFfF2uORht2Lwa5vuM5ZRuxJhbmQLsvbY9RpqJ6JpQ%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13856:<a href="https://link.segmentfault.com/?enc=BzCEr9EQOoafhvtOjBgpag%3D%3D.JXC4%2FteQPpNTCnxo2w2%2BV2883wZLt%2FZ63NTFi8%2FU3COk5KFr1%2FdJjbVKYoOm%2FgXNDGR6J%2BUxz5AAk5ReYIEjlw%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13857:<a href="https://link.segmentfault.com/?enc=frwp9Mn0V8IXbXK5o6J0HA%3D%3D.yn82TDMVLu7veEtLCUSvz51vYXmwBP3HWCn88gmmXL597DIyYIZn1YF5PAEjJq9suYI%2Fl2nimKQpmNENormPxA%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13802:<a href="https://link.segmentfault.com/?enc=uAEjhcsOsSlFaSIizMEDSw%3D%3D.FfIt0A7XGEyPQbR7CsKrkNAO7lXexVUU2jIbn1qRiUttyiSCvbnTKU5JqT2h%2BfLCM0Gt7WDu4eT7vqdQmgMiSQ%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-13852:<a href="https://link.segmentfault.com/?enc=fYIpmDcE%2FOBOZLT0dAiOIQ%3D%3D.nmy%2FWJJwvRGJGEkY44CKKXAToqLg9FMKohdclq1o%2Fn6c8yqwScBAumKh3N6KXOQ4CVURyzwYPwWSUPkwtwcBzw%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a><br>HDFS-14114:<a href="https://link.segmentfault.com/?enc=0h%2BA0PYISbIOc4NaoqRM%2Bw%3D%3D.Q03P1IkeEDlRQwnCN3jv9xMNz%2BioxO%2F5YFtbkXFWWOSfDm3PIPM%2FBcySFyaitp%2FjULaQnEVp2ohHEG4bFWwvQw%3D%3D" rel="nofollow">https://issues.apache.org/jir...</a></p>
<p><strong>4、额外工作</strong></p>
<p>除了贡献给社区的一些工作,内部也有额外的一些对 RBF 的工作。RBF 的 WebUI 目前的显示存在一些问题,节点数量、存储总量的显示为各个集群之和,内部对存储、节点数量的计算做了一些修改;为了更好的对接产品,对外增加了一些 API,方便产品服务通过 API 远程增加 mount table 条目信息,而不只是使用管理工具。</p>
<p><strong>四、总结</strong></p>
<p>Router-Based Federation 方案在滴滴内部已经上线,稳定运行了两个多月的时间了,在运维和产品上提供了极大的便利。未来我们会继续参与社区,为丰富 RBF 的功能以及提高其稳定性贡献一份力量。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
滴滴开源分布式消息中间件产品DDMQ
https://segmentfault.com/a/1190000019234341
2019-05-20T13:15:42+08:00
2019-05-20T13:15:42+08:00
滴滴技术
https://segmentfault.com/u/didijishu
2
<p>出品 | 滴滴技术</p>
<p><img src="/img/bVbsRSQ?w=636&h=275" alt="图片描述" title="图片描述"></p>
<p>滴滴出行消息服务团队近日开源了其内部广泛使用的分布式消息中间件产品 DDMQ,这是一款致力于提供低延迟、高并发、高可用、高可靠消息服务的企业级消息队列产品。</p>
<p><strong>一、产品特性</strong></p>
<p>DDMQ 具有如下的优秀特性:<br>低延迟高吞吐:毫秒级延迟,单机百万条消息吞吐。<br>丰富的消息类型:具备实时消息、延时消息和分布式事务消息。<br>海量消息存储,支持消息回溯消费:支持 RocketMQ 和 Kafka 作为实时消息的存储引擎,使用RocksDB 作为延时消息的存储引擎。<br>秒级延时消息:支持单条消息设置精确到秒级的延迟时间,提供普通延时消息和循环延时消息。<br>多语言客户端,提供了主流开发语言SDK,包括PHP, Java, Go, C/C++, Python,在API 上保持着最易使用的 High Level 形式。<br>多种消费方式:支持通过 Thrift RPC拉取、HTTP 推送和第三方存储直写的方式消费消息。<br>支持灵活的消息过滤和转换功能:通过使用 Groovy 脚本在服务端进行消息体的转换和过滤,能做有效减少客户端和服务器的数据传输量,减轻客户端处理消息的负载。<br>统一的Web控制台:方便用户管理Topic等资源,通过控制台可以实现配置生产和消费的限流值、消费方式、Groovy脚本、启停消费、重置消费进度等功能。<br>完善的监控配套:提供模块的健康检查和消息堆积告警功能。</p>
<p><strong>二、适用场景</strong></p>
<p>消息队列作为构建现代分布式应用所必备的基础设施,有着广泛的应用场景。<br>削峰填谷<br>在秒杀等场景下会导致短时间流量的暴涨,下游系统会因为缺少保护而过载甚至崩溃。DDMQ提供的海量堆积能力和消费限流能够确保下游系统的平稳运行。<br>异步解耦<br>通过上下游系统的松耦合设计,可以保证上游系统不会因为下游系统的宕机而不可用。确保主流程的正常稳定运行。<br>顺序消息<br>现实中需要保证顺序的场景很多,比如订单系统中订单创建、支付、退款等流程,均需要保证顺序。 DDMQ提供的顺序消费功能可以保证消息的先进先出。 <br>事务消息<br>在微服务的场景下,通过DDMQ的事务消息能够达到分布式事务的最终一致性。</p>
<p><strong>三、架构设计</strong></p>
<p>下面这张图描述了DDMQ 的总体架构。主要包括 Broker Cluster、Producer Proxy Cluster(以下简称 PProxy),Consumer Proxy Cluster(以下简称CProxy),SDK,Console 等模块。</p>
<p><img src="/img/bVbsRSW?w=1080&h=607" alt="图片描述" title="图片描述"></p>
<p>Broker Cluster 是DDMQ的消息存储层。使用 RocketMQ作为实时消息的存储引擎(同时也支持使用Kafka),Chronos则是我们基于 RocksDB自研的延时消息存储引擎。<br>PProxy 是DDMQ的生产代理服务, 内置 Thrift RPC Server,生产 SDK 通过RPC 调用将消息发送给 PProxy,然后再由PProxy负责将消息生产到具体的 Broker 中去,在 PProxy 中我们实现了生产限流、重试和消息批量生产等功能。<br>CProxy 是DDMQ的消费代理服务,也内置了Thrift RPC Server,当选择SDK消费时,消费方以 pull 的方式从 CProxy 中拉取消息,由于 CProxy 中的PullBuffer提前缓存了一定数量的待消费消息,因此消费的延迟很低。如果选择HTTP方式消费,则直接由CProxy将消息推送到业务指定的回调URL地址。在CProxy 中,我们实现了消息过滤(通过编写Groovy脚本)、消息体转换(Transit)、重试、消费限流、顺序消费内部排序等功能。<br>Console是DDMQ的控制台,用户通过控制台申请Topic、Group等资源。Topic等数据会持久化到MySQL并推送到 Zookeeper;PProxy和CProxy通过读取、监听 Zookeeper 上的Topic和Group 数据来实时控制消息的生产和消费逻辑。</p>
<p>DDMQ选择Proxy+SDK的架构,主要有这几个好处:<br><strong>方便实现多语言SDK的实现,</strong>由于滴滴内部使用的技术栈比较多,将主要逻辑放在 Proxy 上有利于降低 SDK的复杂度,让SDK的开发速度大大加快。目前在滴滴内部支持PHP, Go , C/C++, Java, Python, Node.js等语言的SDK实现。<br><strong>存储层业务无感知,</strong>由于Proxy层屏蔽了后面的RocketMQ或Kafka,使得存储层的切换可以做到业务无感知。<br><strong>加快新功能迭代速度,</strong>新功能的开发都在 Proxy 层实现,降低了SDK的升级频率。</p>
<p><strong>四、总结</strong></p>
<p>DDMQ 已经在滴滴内部稳定运行了两年多时间,支撑了网约车、小桔车服、地图、金融、智能驾驶、智慧交通、外卖等业务的稳定运行。日消息流水达到千亿级别,整体服务可用性超过5个9。</p>
<p>GitHub 仓库地址:<br><a href="https://link.segmentfault.com/?enc=%2F%2Bmhypk%2BcDDNnkvLR5sr7w%3D%3D.FYY4TUP%2Fa3ES8uih9Q07oGI7%2BniJgjMvir6WarFF89o%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=9WO6JN%2FItgIzDR0ldnihsA%3D%3D.olsgLi2sOIK7r8cVd%2FoxSEHNSgTAJgErqeESZNgjs4Y%3D" rel="nofollow">https://github.com/didi/DDMQ</a> <br>欢迎大家多提issue</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
Nginx多进程高并发、低时延、高可靠机制在滴滴缓存代理中的应用
https://segmentfault.com/a/1190000019234251
2019-05-20T12:57:09+08:00
2019-05-20T12:57:09+08:00
滴滴技术
https://segmentfault.com/u/didijishu
2
<p>出品 | 滴滴技术<br>作者 | 亚洲</p>
<p>一、<strong>开发背景</strong></p>
<p>现有开源缓存代理中间件有twemproxy、codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能。</p>
<p>由于twemproxy无法利用多核特性,因此性能低下,短连接QPS大约为3W,长连接QPS大约为13W; codis起几十个线程,短连接qps不超过10万;同时某些场景这些开源软件时延抖动厉害。</p>
<p>为了适应公有云平台上业务方的高并发需求,因此决定借助于twemproxy来做二次开发,把nginx的高性能、高可靠、高并发机制引入到twemproxy中,通过master+多worker进程来实现七层转发功能。</p>
<p>二、<strong>Twemproxy</strong></p>
<p><strong>2.1 Twemproxy简介</strong></p>
<p>Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII协议和更新的Redis协议。它全部用C写成,使用Apache 2.0 License授权。支持以下特性:<br>1)速度快<br>2)轻量级<br>3)维护持久的服务器连接<br>4)启用请求和响应的管道<br>5)支持代理到多个后端缓存服务器<br>6)同时支持多个服务器池<br>7)多个服务器自动分享数据<br>8)可同时连接后端多个缓存集群<br>9)实现了完整的 memcached ascii 和 redis 协议.<br>10)服务器池配置简单,通过一个 YAML 文件即可<br>11)一致性hash<br>12)详细的监控统计信息<br>13)支持 Linux, *BSD, OS X and Solaris (SmartOS)<br>14)支持设置HashTag<br>15)连接复用,内存复用,提高效率</p>
<p><strong>2.2 滴云memcache缓存集群拓扑结构</strong></p>
<p><img src="/img/bVbsPTg?w=564&h=329" alt="图片描述" title="图片描述"></p>
<p>如上图所示,实际应用中业务程序通过轮询不同的twemproxy来提高qps,同时实现负载均衡。</p>
<p><strong>2.3 推特原生twemproxy瓶颈</strong></p>
<p>如今twemproxy凭借其高性能的优势, 在很多互联网公司得到了广泛的应用,已经占据了其不可动摇的地位, 然而在实际的生产环境中, 存在以下缺陷,如下:<br>i)单进程单线程, 无法充分发挥服务器多核cpu的性能<br>ii)当twemproxy qps短连接达到8000后,消耗cpu超过70%,时延陡增。<br>iii)大流量下造成IO阻塞,无法处理更多请求,qps上不去,业务时延飙升<br>iiii)维护成本高,如果想要充分发挥服务器的所有资源包括cpu、 网络io等,就必须建立多个twemproxy实例,维护成本高<br>iiiii)扩容、升级不便<br>原生twemproxy进程呈现了下图现象:一个人干活,多个人围观。多核服务器只有一个cpu在工作,资源没有得到充分利用。</p>
<p><img src="/img/bVbsPTl?w=439&h=436" alt="图片描述" title="图片描述"></p>
<p><strong>三、Nginx</strong></p>
<p>nginx是俄罗斯软件工程师Igor Sysoev开发的免费开源web服务器软件,聚焦于高性能,高并发和低内存消耗问题,因此成为业界公认的高性能服务器,并逐渐成为业内主流的web服务器。主要特点有:<br>i)完全借助epoll机制实现异步操作,避免阻塞。<br>ii)重复利用现有服务器的多核资源。<br>iii)充分利用CPU 亲和性(affinity),把每个进程与固定CPU绑定在一起,给定的CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性,减少进程调度开销。<br>iiii)请求响应快<br>iiiii)支持模块化开发,扩展性好<br>iiiii)Master+多worker进程方式,确保worker进程可靠工作。当worker进程出错时,可以快速拉起新的worker子进程来提供服务。<br>iiiiii)内存池、连接池等细节设计保障低内存消耗。<br>iiiiii)热部署支持,master与worker进程分离设计模式,使其具有热部署功能。<br>iiiiiii)升级方便,升级过程不会对业务造成任何伤害。</p>
<p>Nginx多进程提供服务过程如下图所示:<br><img src="/img/bVbsPTt?w=562&h=500" alt="图片描述" title="图片描述"></p>
<p><strong>四、Nginx master+worker多进程机制在twemproxy中的应用</strong></p>
<p><strong>4.1 为什么选择nginx多进程机制做为参考?</strong></p>
<p>Twemproxy和nginx都属于网络io密集型应用,都属于七层转发应用,时延要求较高,应用场景基本相同。<br>Nginx充分利用了多核cpu资源,性能好,时延低。</p>
<p><strong>4.2 Master-worker多进程机制原理</strong></p>
<p>Master-worker进程机制采用一个master进程来管理多个worker进程。每一个worker进程都是繁忙的,它们在真正地提供服务,master进程则很“清闲”,只负责监控管理worker进程, 包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。<br>worker进程负责处理客户端的网络请求,多个worker进程同时处理来自客户端的不同请求,worker进程数可配置。</p>
<p><strong>4.3 多进程关键性能问题点</strong></p>
<p>master-worker多进程模式需要解决的问题主要有:<br>i)linux内核低版本(2.6以下版本), “惊群”问题<br>ii) linux内核低版本(2.6以下版本),负载均衡问题<br>iii)linux内核高版本(3.9以上版本)新特性如何利用<br>iii)如何确保进程见高可靠通信<br>iiii)如何减少worker进程在不同cpu切换的开销<br>iiiii)master进程如何汇总各个工作进程的监控数据<br>iiiiii)worker进程异常,如何快速恢复</p>
<p><strong>4.3.1 linux内核低版本关键技术问题</strong></p>
<p>由于linux低内核版本缺陷,因此存在”惊群”、负载不均问题,解决办法完全依赖应用层代码保障。</p>
<p><strong>4.3.1.1 如何解决“惊群”问题</strong></p>
<p>当客户端发起连接后,由于所有的worker子进程都监听着同一个端口,内核协议栈在检测到客户端连接后,会激活所有休眠的worker子进程,最终只会有一个子进程成功建立新连接,其他子进程都会accept失败。</p>
<p>Accept失败的子进程是不应该被内核唤醒的,因为它们被唤醒的操作是多余的,占用本不应该被占用的系统资源,引起不必要的进程上下文切换,增加了系统开销,同时也影响了客户端连接的时延。</p>
<p>“惊群”问题是多个子进程同时监听同一个端口引起的,因此解决的方法是同一时刻只让一个子进程监听服务器端口,这样新连接事件只会唤醒唯一正在监听端口的子进程。</p>
<p>因此“惊群”问题通过非阻塞的accept锁来实现进程互斥accept(),其原理是:在worker进程主循环中非阻塞trylock获取accept锁,如果trylock成功,则此进程把监听端口对应的fd通过epoll_ctl()加入到本进程自由的epoll事件集;如果trylock失败,则把监听fd从本进程对应的epoll事件集中清除。</p>
<p>Nginx实现了两套互斥锁:基于原子操作和信号量实现的互斥锁、基于文件锁封装的互斥锁。考虑到锁的平台可移植性和通用性,改造twemproxy选择时,选择文件锁实现。</p>
<p>如果获取accept锁成功的进程占用锁时间过长,那么其他空闲进程在这段时间内无法获取到锁,从而无法接受新的连接。最终造成客户端连接相应时间变长,qps低,同时引起负载严重不均衡。为了解决该问题,选择通过post事件队列方式来提高性能,trylock获取到accept锁成功的进程,其工作流程如下:</p>
<p>1.trylock获取accept锁成功<br>2.通过epoll_wait获取所有的事件信息,把监听到的所有accept事件信息加入accept_post列表,把已有连接触发的读写事件信息加入read_write_post列表。<br>3.执行accept_post列表中的所有事件<br>4.Unlock锁<br>5.执行read_write_post列表中的事件。<br>Worker进程主循环工作流程图如下:</p>
<p><img src="/img/bVbsPTH?w=958&h=1212" alt="图片描述" title="图片描述"></p>
<p>从上图可以看出,worker进程借助epoll来实现网络异步收发,客户端连接twemproxy的时候,worker进程循环检测客户端的各种网络事件和后端memcached的网络事件,并进行相应的处理。</p>
<p><strong>twemproxy各个进程整体网络i/o处理过程图如下:</strong><br><img src="/img/bVbsPTM?w=1080&h=579" alt="图片描述" title="图片描述"></p>
<p><strong>4.3.1.2 如何解决“负载均衡“问题</strong></p>
<p>在多个子进程争抢处理同一个新连接事件时,一定只有一个worker子进程最终会成功建立连接,随后,它会一直处理这个连接直到连接关闭。这样,如果有的子进程“运气”很好,它们抢着建立并处理了大部分连接,其他子进程就只能处理少量连接,这对多核cpu架构下的应用很不利。理想情况下,每个子进程应该是平等的,每个worker子进程应该大致平均的处理客户端连接请求。如果worker子进程负载不均衡,必然影响整体服务的性能。<br>nginx通过连接阈值机制来实现负载均衡,其原理如下:每个进程都有各自的最大连接数阈值max_threshold和当前连接阈值数local_threshold,和当前连接数阈值,进程每接收一个新的连接,local_threshold增一,连接断开后,local_threashold减一。如果local_threshold超过max_threshold,则不去获取accept锁,把accept机会留给其他进程,同时把local_threshold减1,这样下次就有机会获取accept锁,接收客户端连接了。</p>
<p>在实际业务应用中,有的业务采用长连接和twemproxy建立连接,连接数最大可能就几百连接,如果设置max_threshold阈值过大,多个连接如果同时压到twemproxy,则很容易引起所有连接被同一个进程获取从而造成不均衡。</p>
<p>为了尽量减少负载不均衡,在实际应用中,新增了epoll_wait超时时间配置选项,把该超时时间设短,这样减少空闲进程在epoll_wait上的等待事件,从而可以更快相应客户端连接,并有效避免负载不均衡。</p>
<p><strong>4.3.2 Linux内核高版本TCP REUSEPORT特性如何利用</strong><br>4.3.2.1 什么是reuseport?<br>reuseport是一种套接字复用机制,它允许你将多个套接字bind在同一个IP地址/端口对上,这样一来,就可以建立多个服务来接受到同一个端口的连接。</p>
<p>4.3.2.2 支持reuseport和不支持reuseport的区别<br>如果linux内核版本小于3.9,则不支持reuseport(注:部分centos发行版在低版本中已经打了reuseport patch,所以部分linux低版本发行版本也支持该特性)。</p>
<p>不支持该特性的内核,一个ip+port组合,只能被监听bind一次。这样在多核环境下,往往只能有一个线程(或者进程)是listener,也就是同一时刻只能由一个进程或者线程做accept处理,在高并发情况下,往往这就是性能瓶颈。其网络模型如下:</p>
<p><img src="/img/bVbsPTV?w=554&h=523" alt="图片描述" title="图片描述"></p>
<p>在Linux kernel 3.9带来了reuseport特性,它可以解决上面的问题,其网络模型如下:</p>
<p><img src="/img/bVbsPTY?w=554&h=508" alt="图片描述" title="图片描述"></p>
<p>reuseport是支持多个进程或者线程绑定到同一端口,提高服务器程序的吞吐性能,其优点体现在如下几个方面:<br>i)允许多个套接字 bind()/listen() 同一个TCP/UDP端口<br>ii)每一个线程拥有自己的服务器套接字<br>iii)在服务器套接字上没有了锁的竞争,因为每个进程一个服务器套接字<br>iiii)内核层面实现负载均衡<br>iiiii)安全层面,监听同一个端口的套接字只能位于同一个用户下面</p>
<p><strong>4.3.3 Master进程和worker进程如何通信?</strong></p>
<p>由于master进程需要实时获取worker进程的工作状态,并实时汇总worker进程的各种统计信息,所以选择一种可靠的进程间通信方式必不可少。</p>
<p>在twemproxy改造过程中,直接参考nginx的信号量机制和channel机制(依靠socketpair)来实现父子进程见通信。Master进程通过信号量机制来检测子进程是否异常,从而快速直接的反应出来;此外,借助socketpair,封装出channel接口来完成父子进程见异步通信,master进程依靠该机制来统计子进程的各种统计信息并汇总,通过获取来自master的汇总信息来判断整个twemproxy中间件的稳定性、可靠性。</p>
<p>配置下发过程:主进程接收实时配置信息,然后通过channel机制发送给所有的worker进程,各个worker进程收到配置信息后应答给工作进程。流程如下:</p>
<p><img src="/img/bVbsPT5?w=613&h=490" alt="图片描述" title="图片描述"></p>
<p>获取监控信息流程和配置下发流程基本相同,master进程收到各个工作进程的应答后,由master进程做统一汇总,然后发送给客户端。</p>
<p><strong>4.3.4 如何减少worker进程在不同cpu切换的开销</strong></p>
<p>CPU 亲和性(affinity) 就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。</p>
<p>Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。具体参考sched_setaffinity函数。</p>
<p><strong>4.3.5 worker进程异常如何减少对业务的影响?</strong></p>
<p>在实际线上环境中,经常出现这样的情况:某个多线程服务跑几个月后,因为未知原因进程挂了,最终造成整个服务都会不可用。</p>
<p>这时候,master+多worker的多进程模型就体现了它的优势,如果代码有隐藏的并且不容易触发的bug,某个时候如果某个请求触发了这个bug,则处理这个请求的worker进程会段错误退出。但是其他worker进程不会收到任何的影响,也就是说如果一个改造后的twemproxy起了20个worker进程,某个时候一个隐藏bug被某个请求触发,则只有处理该请求的进程段错误异常,其他19个进程不会受到任何影响,该隐藏bug触发后影响面仅为5%。如果是多线程模型,则影响面会是100%。</p>
<p>如果某个worker进程挂了,master父进程会感知到这个信号,然后重新拉起一个worker进程,实现瞬间无感知”拉起”恢复。以下为模拟触发异常段错误流程:</p>
<p><img src="/img/bVbsPUh?w=737&h=498" alt="图片描述" title="图片描述"></p>
<p>如上图所示,杀掉31420 worker进程后,master进程会立马在拉起一个31451工作进程,实现了快速恢复。</p>
<p>多进程异常,自动”拉起”功能源码,可以参考如下demo:<br><a href="https://link.segmentfault.com/?enc=D3D%2BGmM1PZFRdZ%2B19vwjVw%3D%3D.Uu%2BwvvDkE1iaormGcYNzjycSiaLL8Pwea4gqee7YZUQywvtIlm14Qn3h1zvJQv2Zm4VTIsJ06NQsUKOpVIEUz%2Bt056zQkZj%2F3QpFigNRWvWu40pMqA0a70ChFhaLJszT" rel="nofollow">https://github.com/y123456yz/...</a></p>
<p><strong>五、网络优化</strong></p>
<p>5.1 网卡多队列<br>在实际上线后,发现软中断过高,几乎大部分都集中在一个或者几个CPU上,严重影响客户端连接和数据转发,qps上不去,时延抖动厉害。</p>
<p>RSS(Receive Side Scaling)是网卡的硬件特性,实现了多队列,可以将不同的流分发到不同的CPU上。支持RSS的网卡,通过多队列技术,每个队列对应一个中断号,通过对每个中断的绑定,可以实现网卡中断在cpu多核上的分配,最终达到负载均衡的作用。</p>
<p>5.2 可怕的40ms<br>原生twemproxy在线上跑得过程中,发现时延波动很大,抓包发现其中部分数据包应答出现了40ms左右的时延,拉高了整体时延抓包如下(借助tcprstat工具):</p>
<p><img src="/img/bVbsRQR?w=553&h=29" alt="图片描述" title="图片描述"></p>
<p>解决办法如下:在recv系统调用后,调用一次setsockopt函数,设置TCP_QUICKACK。代码修改如下:</p>
<p><img src="/img/bVbsRQW?w=542&h=41" alt="图片描述" title="图片描述"></p>
<p><strong>六、Twemproxy改造前后性能对比 (时延、qps对比)</strong></p>
<p><strong>6.1 线上真实流量时延对比</strong><br><strong>6.1.1 改造前线上twemproxy集群时延</strong><br>线上集群完全采用开源twemproxy做代理,架构如下:</p>
<p><img src="/img/bVbsRQ5?w=1061&h=490" alt="图片描述" title="图片描述"></p>
<p>未改造前线上twemproxy+memcache集群,qps=5000~6000,长连接,客户端时延分布如下图所示:</p>
<p><img src="/img/bVbsRRe?w=554&h=134" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsRRg?w=554&h=129" alt="图片描述" title="图片描述"></p>
<p>在twemproxy机器上使用tcprstat监控到的网卡时延如下:</p>
<p><img src="/img/bVbsRRo?w=523&h=494" alt="图片描述" title="图片描述"></p>
<p>从上面两个图可以看出,采用原生twemproxy,时延高,同时抖动厉害。</p>
<p><strong>6.1.2 参照nginx改造后的twemproxy时延</strong><br>线上集群一个twemproxy采用官方原生twemproxy,另一个为改造后的twemproxy,其中改造后的twemproxy配置worker进程数为1,保持和原生开源twemproxy进程数一致,架构如下:</p>
<p><img src="/img/bVbsRRp?w=1043&h=469" alt="图片描述" title="图片描述"></p>
<p>替换线上集群两个代理中的一个后(影响50%流量),长连接,qps=5000~6000,客户端埋点监控时延分布如下:</p>
<p><img src="/img/bVbsRRw?w=554&h=141" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsRRx?w=554&h=135" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsRRA?w=554&h=169" alt="图片描述" title="图片描述"></p>
<p>替换两个proxy中的一个后,使用tcprstat在代理集群上面查看两个代理的时延分布如下:</p>
<p>原生twemproxy节点机器上的时延分布:</p>
<p><img src="/img/bVbsRRK?w=548&h=461" alt="图片描述" title="图片描述"></p>
<p>另一个改造后的twemproxy节点机器上的时延分布:</p>
<p><img src="/img/bVbsRRM?w=541&h=487" alt="图片描述" title="图片描述"></p>
<p>总结:替换线上两个proxy中的一个后,客户端时间降低了一倍,如果线上集群两个代理都替换为改造后的twemproxy,客户端监控时延预计会再降低一倍,总体时延降低3倍左右。<br>此外,从监控可以看出,改造后的twemproxy时延更低,更加稳定,无任何波动。</p>
<p><strong>6.2 参考nginx多进程改造后的twemproxy线下压测结果(开启reuseport功能)</strong><br>监听同一个端口,数据长度100字节,压测结果如下:<br> linux内核版本:linux-3.10<br> 物理机机型: M10(48 cpu)<br><img src="/img/bVbsRRT?w=474&h=512" alt="图片描述" title="图片描述"></p>
<p>多进程监听同一个端口,数据长度150字节,压测结果如下:<br> linux内核版本:linux-3.10<br> 物理机机型: TS60 (24 cpu)</p>
<p><img src="/img/bVbsRRU?w=288&h=491" alt="图片描述" title="图片描述"></p>
<p><strong>七、总结</strong><br><strong>7.1 多进程、多线程机制选择</strong><br>选择参照nginx多进程机制,而不选择多线程实现原因主要有:<br>1) 多进程机制无锁操作,实现更容易<br>2) 多进程的代理,整个worker进程无任何锁操作,性能更好<br>3) 如果是多线程方式,如果代码出现bug段错误,则整个进程挂掉,整个服务不可用。而如果是多进程方式,因为bug触发某个worker进程段错误异常,其他工作进程不会受到如何影响,20个worker进程,如果触发异常,同一时刻只有有1/20的流量受到影响。而如果是多线程模式,则100%的流量会受到影响。<br>4) worker进程异常退出后,master进程立马感知拉起一个新进程提供服务,可靠性更高。<br>5) 配置热加载、程序热升级功能实现更加容易</p>
<p><strong>7.2 参照nginx改造后的twemproxy特性</strong><br>支持nginx几乎所有的优秀特性,同时也根据自己实际情况新增加了自有特性:<br>1) master+多worker进程机制<br>2) 适配所有linux内核版本,内核低版本惊群问题避免支持<br>3) quic_ack支持<br>4) reuser_port适配支持<br>5) worker进程异常,master进程自动拉起功能支持<br>6) 90%、95%、98%、100%平均时延统计功能支持<br>7) memcache单机版、集群版支持<br>8) redis单机版、集群版支持<br>9) 二进制协议、文本协议同时支持<br>10) redis、memcache集群在线扩容、缩容、数据迁移支持,扩缩容、数据迁移过程对业务无任何影响。<br>11) 多租户支持,一个代理可以接多个memcache、redis集群,并支持混部。<br>12) mget、gets、sets等批量处理命令优化处理<br>13) 慢响应日志记录功能支持<br>14) 内存参数实时修改支持<br>15) 详细的集群监控统计功能<br>16) CPU亲缘性自添加<br>17)内存配置动态实时修改</p>
<p><strong>7.3后期计划</strong><br> 添加如下功能:</p>
<pre><code> i) 配置文件热加载支持。
ii) 代码热升级功能支持。
</code></pre>
<p><strong>7.4 长远规划展望</strong><br>抽象出一款类似nginx的高性能代理软件,nginx支持http协议,我们的支持tcp协议代理,覆盖nginx所有功能,包括前面提到的所有功能,同时支持模块化开发。这样,很多的tcp协议代理就无需关心网络架构底层实现,只需要根据需要开发对应的协议解析模块,和自己关心的统计、审计等功能功能,降低开发成本。现有开源的中间件,很大一部分都是tcp的,有自己的私有tcp协议,把这个抽象出来,开发成本会更低。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
运维平台信用分——滴滴内部的数据驱动实践
https://segmentfault.com/a/1190000019216640
2019-05-17T18:28:02+08:00
2019-05-17T18:28:02+08:00
滴滴技术
https://segmentfault.com/u/didijishu
2
<p>出品 | 滴滴技术<br>作者 | 张健<br><img src="/img/bVbsNdB?w=1280&h=544" alt="图片描述" title="图片描述"></p>
<p>在大家的印象中,运维人员更多的是从属业务的角色。在传统的企业IT中,没有快速的产品迭代,没有每天成百上千次的服务发布和伸缩容,这样的角色看似没有问题。但在如今的 DevOps 时代,日常的运维工作中每天要应对成百上千次的服务发布与线上操作。如果运维人员(即SRE)仍然只是被动的去应对这种变化,所造成的结果,必然是疲于应付,最终会对全平台的业务稳定性造成很大隐患。</p>
<p>那么,在这种量变引起质变的挑战中,运维人员应该发挥怎样的作用,才能适应新业务的挑战呢?笔者之前曾就职于IBM Cloud部门,现在就职于滴滴运维部,长期从事自动化运维方面的工作,下面就结合自己之前的经验和目前的工作,谈谈自己的一些见解。</p>
<p><strong>一. 来自业务的挑战</strong></p>
<p>无论是在滴滴还是在之前的部门,在业务发展的初期阶段,都不可避免的经历了粗犷型的扩张阶段,比如业务量指数级上升,用户量急剧增加,每时每刻都有服务模块的迭代。</p>
<p>在业务优先的前提下,运维人员承担着巨大的运维压力。以监控为例,用户添加监控不规范,会造成报警频发,报警有效性不足,导致的后果就是容易让真正有价值的报警湮没在海量数据中,同时,也会造成对报警资源的浪费,比如,研发同学不区分测试、线上环境,随意的添加报警采集指标,会对监控系统的存储,查询带来极大的挑战。再比如部署系统,不按照规范,在高峰期更新服务,一旦出问题,会造成整个应用的服务不可用。这样的例子有很多。</p>
<p><strong>二. 如何应对</strong></p>
<p>如果上述的问题一直延续下去,运维工作必然带来巨大的挑战,并且会严重影响线上服务的稳定性。面对这些问题,滴滴运维团队的同学也在一起思考,运维应该不仅仅去被动的适应业务,而是要从平台稳定性出发,去指导研发同学,如何规范的执行变更,如何合理的使用监控资源以及其它公司IT基础设施。</p>
<p>我们想到的解决方法就是“数据说话”,尽可能的去量化监控、部署及基础组件(MySQL, Codis, ZK)的使用。然后用数字去指导研发的同学,尽可能的去匹配我们给出的“最佳实践”,从而减少造成线上业务不稳定的隐患。</p>
<p>所以,滴滴运维部推出了“风险量化平台”,包含“变更信用分”(用来度量服务的变更操作,比如服务部署上线,配置变更等)、“监控健康分”(用来度量用户对报警监控的使用),从而打造一个“看得见的手”,驱动业务同学来一起提高线上稳定性。</p>
<p><strong>| 数据驱动的难点有三个方面</strong></p>
<p>首先是如何获取数据?这是“风险量化平台”的基础。使用监控系统,部署一个服务,执行一次配置变更,都是一个个用户操作,很难用数字去表达。为此我们结合运维经验,基于对操作每个步骤的详尽输出,近可能的去用数字维度来衡量用户操作。比如以部署为例,会以灰度发布中间的暂停时间是否满足一定时长,是否有在上线高峰期操作记录,部署过程中是否执行了double-check,在哪个阶段执行了回滚等等,来形成一个个的打分项。</p>
<p>其次是如何去制定风险量化的标准,也就是如何用各个指标去构造一个最佳实践。这更像是一个数学建模,里面涉及到大量的运维经验积累,以我们新推出的监控健康分为例,我们遵循着“有服务必有监控,有报警必须处理”的原则,对于每个服务,要求衡量的标准包括,是否有存活指标监控(进程、端口等);是否有基础指标监控(如cpu.idle,mem.used, disk.used);是否添加了上下游监控,报警是否有效,即报警接收人是否过多(因为大家都收到报警,最终的结果,往往意味着大家都不会处理报警),报警是否被及时处理(运维领域也有MTTA, MTTR,即报警平均响应时间,和报警及时处理时间这样的概念);是否配置了监控大盘,方便我们日常巡检。</p>
<p>各个量化项目占据不同的权重(如下方的监控健康分剖析图), 比如我们根据滴滴目前的服务特点,存活指标占比40%, 报警有效性占比30%,推动业务去收敛报警,和完善监控。监控健康分以80分为及格线,寻找出监控漏洞,并指导用户加以改进。 用这样的方法,可以让研发同学尽可能的减少漏配监控的事情发生,提高线上服务的稳定性。</p>
<p><img src="/img/bVbsNg5?w=972&h=926" alt="图片描述" title="图片描述"></p>
<p>最后的难点是如何驱动?这是我们现在着力想的一个点。风险量化实际上就是总结前人踩过的坑,趟过的雷,去告诉后面的同学,提前来规避风险,这是运维部门对公司业务稳定性的一大贡献。</p>
<p>现在已有的做法是如下图(各部门变更信用分排名图)所示,通过计算、打分、全公司各个业务线排名,将风险量化数据和反应出的问题推送给各个业务线的leader。以竞赛方式去推动各个业务线重视风险量化。我们还计划以监控健康分去驱动报警有效性的建设,完善报警值班制度,避免群发报警又无人处理,报警配置不合理这种现象的发生。</p>
<p><img src="/img/bVbsNhf?w=633&h=1112" alt="图片描述" title="图片描述"></p>
<p><strong>三. 效果如何</strong></p>
<p>目前的风险量化体系包含“变更信用分”,“监控健康分”,其中变更信用分已经上线一年多了,在2018年,从下图能明显看到信用分在稳步上升。</p>
<p><img src="/img/bVbsNhw?w=880&h=294" alt="图片描述" title="图片描述"></p>
<p>带来结果是什么呢? 下面是本年度故障case统计图,能明显的看到这种趋势,故障case数量随着变更信用分的提高在稳步下降。考虑到同时期的变更数量也在一直增加,这种下降趋势就更加明显了。</p>
<p><img src="/img/bVbsNhE?w=1258&h=442" alt="图片描述" title="图片描述"></p>
<p>我们期望其它的信用分机制,也能给业务稳定性带来这样积极的结果。</p>
<p><strong>四、未来展望</strong></p>
<p>对于未来的展望,首先希望能对尽可能多的涉及线上操作的内容进行风险量化,比如业务使用的中间件/基础组件,业务中涉及安全的服务是否遵循了相应的规范,是否有密码/数据泄漏风险。</p>
<p>其次,我们仍然需要对已有的运维经验进行总结,结合经验,利用量化分数去构建“最佳实践”,指导大家去遵守。</p>
<p>最后是如何去驱动,将总结的数据价值,最大化的发挥出来。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
使用 OVS-DPDK 加速滴滴云网络
https://segmentfault.com/a/1190000019216344
2019-05-17T18:03:26+08:00
2019-05-17T18:03:26+08:00
滴滴技术
https://segmentfault.com/u/didijishu
7
<p>出品 | 滴滴技术<br>作者 | 张军伟</p>
<p><img src="/img/bVbsNah?w=1800&h=766" alt="图片描述" title="图片描述"></p>
<p>在基于现有 OVS-DPDK 开源软件基础上,滴滴云技术团队通过创新性的改进,实现了ms级别的热升级, 同时保持现有的高性能转发能力。</p>
<p><strong>背景</strong><br>滴滴云初期采⽤学习OpenStack的思路,采用内核态的OVS实现了SDN OverLay网络。这个实践过程中,我们也遇到了一些问题,可以归纳为以下几个⽅面:</p>
<ul>
<li>性能不高</li>
<li>⽆法热升级</li>
<li>开发难度⼤</li>
<li>维护成本高</li>
</ul>
<p><strong>原型设计</strong></p>
<p>针对这些问题,经过技术调研,也参考了国内外同⾏的已有解决方案,在过程中和Intel团队紧密合作,我们采用OVS-DPDK进⾏开发部署,并最终取得了不错的效果。如下是数据流模型:</p>
<p><img src="/img/bVbsNaP?w=1052&h=660" alt="图片描述" title="图片描述"></p>
<p><strong>| 数据层面的几个重要改造</strong></p>
<p><strong>1. 底层:⽹卡硬件相关</strong></p>
<p><strong>- 基于VF的数据流改造</strong></p>
<p>借助硬件将OverLay的流量与宿主机的其他⽹络流量进⾏分离。通过flow classification命令将前者导⼊到VF上,而后者仍然是通过PF口到内核进行处理,保持原有处理逻辑不变。OVS- DPDK只接管VF⽹口,⽽不触碰PF口上的非SDN OverLay⽹络流量。这样既简化了OVS-DPDK的处理逻辑,同时也避免了因OVS-DPDK本身的稳定性,而影响其他⾮SDN⽹络模块的稳定性。</p>
<p><strong>2. 中间层:OVS 报⽂处理</strong></p>
<p><strong>- 无状态的转发功能</strong></p>
<p>⽬前,我们对计算节点⽹络层面的需求,可以分为两大类:VM流量的转发和VM网络的安全监控。其中后者是内部开发的,暂时略过。</p>
<p>针对VM流量转发的这个需求,⼜拆解为两部分:<br>OverLay外层头的处理,和内层报文的转发。借助OVS-DPDK的flow表实现这两部分功能。因为没有启⽤conntrack功能,因此我们这部分的实现是⽆状态的。这个拆解,特别是无状态的特性,在热升级的时候取得了不错的效果。</p>
<p><img src="/img/bVbsNbF?w=1252&h=412" alt="图片描述" title="图片描述"></p>
<ul><li><strong> OVS单网桥</strong></li></ul>
<p>之前参考OpenStack的模型,我们使⽤了br-int,br-tunnel两个网桥。在这个模型里,OVS⽹桥的使⽤方式跟传统的Bridge使⽤⽅式差别不大,没有充分发挥OVS⽹桥的优势。 </p>
<p>在我们的模型中,把两个网桥整合为一个网桥,将vxlan⼝和vhost-user的⼝都放到⽤⼀个网桥上。VM发出的报⽂经过OVS转发处理后,携带外层头信息进入vxlan驱动,经过vxlan网口的封装后,发送给VF网口。</p>
<p>根据我们的数据模型,进入VF⼝的报⽂只可能是发往VM的vxlan类型的报文。这些报文,在被剥除vxlan头后,经过vxlan⼝进⼊网桥,经过⽹桥转发到各个VM的vport。</p>
<ul><li><strong>降低与内核的耦合性</strong></li></ul>
<p>原有的OVS桥的路由和ARP表需要去内核查询,跟内核的耦合性很强。我们通过SDN控制器下发到OVS-DPDK,来规避直接与内核的交互。这样⼀方⾯简化了Bridge的配置(不用单独设置IP地址等),降低了内核的耦合性,另⼀方⾯也降低了热升级时候的复杂度。</p>
<p><strong>3. 上层vhost-user与VM交互层</strong></p>
<ul><li><strong>vhost-user增强</strong></li></ul>
<p>我们使用的是QEMU作为vhost-user的Server端,OVS-DPDK进程通过unixsocket连接到QEMU。QEMU默认仅支持一个这样的连接,改造QEMU后,使得QEMU支持两个主备倒换的连接,这样热升级的时候,可以通过控制OVS-DPDK端的开关,轻松的在新⽼两个进程间切换。</p>
<ul><li><strong>内存模型采用2M/4k</strong></li></ul>
<p>尽量减少对现有VM的影响,为以后升级和迁移做准备。<br><img src="/img/bVbsNch?w=1078&h=652" alt="图片描述" title="图片描述"></p>
<p><strong>1. 升级时间短</strong></p>
<p>业内⽬前的热升级方案基本都是秒级的,切换时间⽐较长。而在我们的框架下,每个VM的热升级时间大约80ms左右,极大的缩短了VM的网络中断时间,基本做到⽤户无感知。</p>
<p><strong>2. 可扩展性好</strong></p>
<p>热升级过程中,VM⽹络中断时间跟VM规模无关。热升级的时候,我们逐个把VM的流量从⽼的OVS-DPDK进程里,切换到新的进程里。这种逐个切换的模式,使得单个VM的流量切换,不会影响其他的VM网络功能。即使上百个VM,总的升级时间达到⼏秒甚至⼏十秒的情况下,单个VM的⽹络中断时间仍然是80ms。</p>
<p><strong>3. 故障恢复快</strong></p>
<p>我们热升级的模型中采⽤的是两个独⽴的OVS-DPDK进程,因此提前启动一个新的OVS-DPDK进程作为后备进程,这个进程完成所有热升级相关的初始化,比如初始化vf2。这样当原有OVS-DPDK进程Crash后,新的进程可以做快速的切换,实验室环境下,单VM测试可以做到跟热升级时间差不多。</p>
<p><strong>附上测试数据</strong><br>性能:单核性能400wpps左右<br>热升级:单VM网络中断时间 80ms 左右<br>DPDK version:17.11<br>OVS version:2.9.0<br>QEMU version:2.9<br>CPU:Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz kernel:3.10.0-514.16.1.el7.x86_64<br>NIC:Ethernet Controller X710 for 10GbE SFP+ 1572</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>
滴滴 Elasticsearch 多集群架构实践
https://segmentfault.com/a/1190000019216131
2019-05-17T17:48:02+08:00
2019-05-17T17:48:02+08:00
滴滴技术
https://segmentfault.com/u/didijishu
44
<p>出品 | 滴滴技术<br>作者 |魏子珺</p>
<p><img src="/img/bVbsMkj?w=1800&h=766" alt="图片描述" title="图片描述"></p>
<p>Elasticsearch 是基于 Lucene 实现的分布式搜索引擎,提供了海量数据实时检索和分析能力。Elastic 公司开源的一系列产品组成的Elastic Stack,可以为日志服务、搜索引擎、系统监控等提供简单、易用的解决方案。</p>
<p><strong>滴滴 Elasticsearch 简介</strong><br>滴滴2016年初开始构建Elasticsearch平台,如今已经发展到超过3500+Elasticsearch实例,超过5PB的数据存储,峰值写入tps超过了2000w/s的超大规模。</p>
<p>Elasticsearch在滴滴有着非常丰富的使用场景,例如线上核心的打车地图搜索,客服、运营的多维度查询,滴滴日志服务等近千个平台用户。</p>
<p>超大的规模和丰富的场景给滴滴Elasticsearch平台带来了极大的挑战,我们在这期间积累了丰富经验,也取得了一些成果。本文给大家分享下滴滴在Elasticsearch多集群架构的实践。</p>
<p><strong>单集群架构瓶颈</strong><br>介绍单集群架构瓶颈前,先来看下滴滴Elasticsearch单集群的架构。</p>
<p><strong>滴滴Elasticsearch单集群架构</strong></p>
<p><img src="/img/bVbsMlH?w=931&h=274" alt="图片描述" title="图片描述"></p>
<p>滴滴在单集群架构的时候,写入和查询就已经通过Sink服务和Gateway服务管控起来。</p>
<p><strong>| Sink服务</strong><br>滴滴几乎所有写入Elasticsearch的数据都是经由kafka消费入到Elasticsearch。kafka的数据包括业务log数据、mysql binlog数据和业务自主上报的数据,Sink服务将这些数据实时消费入到Elasticsearch。</p>
<p>最初设计Sink服务是想对写入Elasticsearch集群进行管控,保护Elasticsearch集群,防止海量的数据写入拖垮Elasticsearch,之后我们也一直沿用了Sink服务,并将该服务从Elasticsearch平台分离出去,成立滴滴Sink数据投递平台,可以从kafka或者MQ实时同步数据到Elasticsearch、HDFS、Ceph等多个存储服务。</p>
<p>有了多集群架构后,Elasticsearch平台可以消费一份MQ数据写入多个Elasticsearch集群,做到集群级别的容灾,还能通过MQ回溯数据进行故障恢复。</p>
<p><strong>| Gateway服务</strong><br>所有业务的查询都是经过Gateway服务,Gateway服务实现了Elasticsearch的http restful和tcp协议,业务方可以通过Elasticsearch各语言版本的sdk直接访问Gateway服务,Gateway服务还实现了SQL接口,业务方可以直接使用SQL访问Elasticsearch平台。</p>
<p>Gateway服务最初提供了应用权限的管控,访问记录,限流、降级等基本能力,后面随着平台演进,Gateway服务还提供了索引存储分离、DSL级别的限流、多集群灾备等能力。</p>
<p><strong>| Admin服务</strong><br>整个Elasticsearch平台由Admin服务统一管控起来。Admin服务提供了索引的生命周期管理,索引容量自动规划,索引健康分,集群监控等丰富的平台能力,以及为Sink、Gateway服务提供索引、权限等元数据信息。</p>
<p><strong>Elasticsearch单集群瓶颈</strong><br>随着滴滴Elasticsearch平台规模的快速发展,Elasticsearch集群越来越大,最大的时候,是由几百台物理机组成集群,当时集群共 3000+ 的索引,超过了 50000 个 shard,集群总容量达到了PB级别。超大的Elasticsearch集群面临了很大的稳定性风险,这些风险主要来自于以下三个方面:</p>
<ul>
<li>Elasticsearch架构瓶颈</li>
<li>索引资源共享风险</li>
<li>业务场景差异大</li>
</ul>
<p><strong>Elasticsearch架构瓶颈</strong><br>Elasticsearch架构在集群变大到一定的规模会遇到瓶颈,瓶颈主要跟Elasticsearch任务处理模型有关。</p>
<p>Elasticsearch看起来是p2p架构,但实际上,仍然是中心化的分布式架构。整个集群只有一个active master。master负责整个集群的元数据管理。集群的所有元数据保存在ClusterState对象中,主要包括全局的配置信息、索引信息和节点信息。只要元数据发生修改,都得由master完成。</p>
<p>Elasticsearchmaster的任务处理是单线程完成的,每次处理任务,涉及到ClusterState的改动,都会将最新的ClusterState对象publish给集群的全部节点,并阻塞等待全部节点接受到变更消息,处理完变更任务后,才完成本次任务。</p>
<p>这样的架构模型导致在集群规模变大的时候出现很严重的稳定性风险。</p>
<ul>
<li>如果有节点假死,比如jvm内存被打满,进程还存活着,响应master任务时间会很长,影响单个任务的完成时间。</li>
<li>有大量恢复任务的时候,由于master是单线程处理的,所有任务需要排队处理,产生大量的pending_tasks。恢复时间变得很长。</li>
<li>Elasticsearch的任务分了优先级,例如put-mapping任务优先级低于创建、恢复索引,如果一些业务上低优先级索引在恢复,正常索引有新字段写入时会被阻塞。</li>
<li>master任务处理模型,在任务执行完成后,会回调大量listener处理元数据变更。其中有些回调逻辑在索引、shard膨胀后,会出现处理缓慢的问题,当shard膨胀到5-6w时,一些任务处理需要8-9s的时间,严重影响了集群的恢复能力。</li>
</ul>
<p>针对这些问题,Elasticsearch也在不断优化,针对相同类型的任务,比如put-mapping任务,master会一次性处理所有堆积在队列里的相同任务。ClusterState对象只传递diff内容,优化回调listener模块的处理耗时环节等等。</p>
<p>但是由于整个集群的任务都集中在一个master的一个线程中处理,在线程中需要同步元数据变更给集群的每个节点,并阻塞等待全部节点同步完成。这个模型在集群规模不断膨胀时,稳定性会不断下降。</p>
<p><strong>| 索引资源共享风险</strong><br>Elasticsearch索引是由多个shard组成,master会动态给这些shard分配节点资源。不同的索引会存在资源混部的情况。<br><img src="/img/bVbsMlW?w=923&h=400" alt="图片描述" title="图片描述"></p>
<p>Elasticsearch通过Shard Allocation Awareness的设计,可以将集群的节点按集合划分成不同的rack。在分配索引时可以指定rack列表,这样索引就只会分配在指定rack对应的节点列表中,从而做到物理资源的隔离。</p>
<p>但是实际使用中,很多容量小的索引由于占用资源有限,会混部在一些节点中。这种情况下,会因为个别索引的查询、写入量飙升,而影响到其他索引的稳定性。如果出现了节点故障,就会影响到整个集群的稳定性。</p>
<p>整个集群master、clientnode资源是共享的,master风险前面已经单独提及,clientnode共享带来的gc、抖动、异常问题都会影响到集群内的全部索引。</p>
<p><strong>| 业务场景差异大</strong><br>Elasticsearch适用的业务场景差异特别大。</p>
<ul>
<li>针对线上核心的入口搜索,一般按城市划分索引后,索引容量不大,数据没有实时写入或者实时写入tps很小,比如地图poi数据采用离线更新的方式,外卖商家、菜品写入量也很小。但是查询的qps很高,查询对rt的平均时间和抖动情况要求很高。</li>
<li>针对日志检索的的场景,实时写入量特别大,有些索引甚至超过了100w/s的tps,该场景对吞吐量要求很高,但对查询qps和查询rt要求不高。</li>
<li>针对binlog数据的检索,写入量相比日志会小很多,但是对查询的复杂度、qps和rt有一定的要求。</li>
<li>针对监控、分析类的场景,聚合查询需求会比较多,对Elasticsearch内存压力较大,容易引起节点的抖动和gc。</li>
</ul>
<p>这些场景各异,稳定性、性能要求各不相同的场景,一个Elasticsearch集群即使使用各种优化手段,很难全部满足需求,最好的方式还是按业务场景划分Elasticsearch集群。</p>
<p><strong>多集群挑战</strong><br>正是单集群面临了非常大的稳定性风险,我们开始规划多集群的架构。我们在设计多集群方案的时候,期望对业务方是零感知的。</p>
<p>写入还是经过kafka,Sink服务可以将不同topic的数据入到不同的Elasticsearch集群。查询继续通过Gateway服务,而且业务方仍然像之前一样传递索引名称,而无需感知到平台内部的索引分布。所有的索引在不同集群的分布细节,均由Gateway服务屏蔽。</p>
<p>整个改造最大的挑战在于查询方式的兼容。Elasticsearch查询索引的方式非常灵活,可以支持*号作为通配符匹配。这样一个索引query可能查询的是多个索引,比如有如下3个索引:</p>
<ul>
<li>index_a</li>
<li>index_b</li>
<li>index_c</li>
</ul>
<p>使用index*查询的时候,可以同时查询到index_a、index_b、index_c三个索引。 Elasticsearch这种实现方式非常简单,由于一次query最终查询的是多个shard的数据,所以无论对于具体的索引,还是模糊的索引,都是先根据索引名称得到shard列表,再将多个shard的query结果merge到一起返回。</p>
<p>这样的使用方式,对于多集群方案就会遇到问题,比如index_a在A集群,index_b在B集群、index_c在C集群,对于index*的query,就无法在一个集群上完成。</p>
<p><strong>tribenode介绍</strong></p>
<p>经过调研,我们发现Elasticsearchtribenode特性可以很好的满足多集群查询的特性。tribenode的实现非常巧妙。org.elasticsearch.tribe包下只有三个文件,核心类是TribeService。tribenode的核心原理就是merge每个集群的ClusterState对象成一个公共的ClusterState对象,ClusterState包含了索引、shard和节点数据分布表。而Elasticsearch的工作逻辑都是基于ClusterState元数据驱动的,所以对外看起来就是一个包含全部索引的的clientnode。</p>
<p><img src="/img/bVbsM7K?w=592&h=573" alt="图片描述" title="图片描述"></p>
<p>tribenode通过配置多个Elasticsearch集群地址,然后以clientnode角色分别连接每个集群,每个集群看起来会多了一个clientnode。tribenode通过该clientnode角色获取到集群的ClusterState信息,并绑定listener监听ClusterState变化。tribenode将获取的所有集群的ClusterState信息merge到一起,形成一个对外部访问使用的ClusterState对象,对外提供服务。tribenode除了注册listener和merge ClusterState,其他的所有逻辑都是复用了clientnode的代码。</p>
<p>可以看到tribenode的优点:</p>
<ul>
<li>能够满足多集群访问的需求,对外使用是透明的。</li>
<li>实现的简单、优雅,可靠性有保证。</li>
</ul>
<p>同时tribenode有些不足的地方:</p>
<ul>
<li>tribenode必须以clientnode加入到每个Elasticsearch集群,master的变更任务必须等待tribenode的回应才能继续,可能影响到原集群的稳定性。</li>
<li>tribenode不会持久化ClusterState对象,重启时需要从每个Elasticsearch集群获取元数据。而在获取元数据期间,tribenode就已经能够提供访问,会导致查询到还在初始化中的集群索引访问失败。</li>
<li>tribenode连接的集群多了,初始化会变得很慢。针对该缺陷,我们平台在重启某个tribenode集群时,将Gateway访问该集群的全部流量切到备份tribenode集群解决。</li>
</ul>
<p>如果多个集群有相同的索引名称,tribenode只能设置一种perfer规则:随机、丢弃、prefer指定集群。这可能带来查到不符合预期的异常。滴滴Elasticsearch平台通过统一管控索引,避免了同一个索引名称出现在tribenode连接的多个集群中。</p>
<p>正是tribenode有了这些瑕疵,Elasticsearch在高版本引入了Cross ClusterSearch的设计,Cross Cluster不会以节点的形式连接到其他集群,只是将请求代理。目前我们还在评估Cross Cluster的方案,这里不展开介绍。</p>
<p><strong>多集群架构拓扑</strong><br>最终改造后,我们的集群架构拓扑如下:</p>
<p><img src="/img/bVbsM8B?w=1273&h=730" alt="图片描述" title="图片描述"></p>
<p>按照不同的应用场景,平台将Elasticsearch集群划分成四种类型,Log集群、Binlog集群、文档数据集群、独立集群。公共集群一般最多100台datanode为基准组成一个集群。我们利用滴滴云实现了集群的自动化部署和弹性扩缩容,可以很方便的水平扩展集群。</p>
<p>Elasticsearch集群前面是多组tribenode集群,主要是为了解决tribenode的稳定性问题。</p>
<p>Gateway会同时连接tribenode集群和Elasticsearch集群,根据应用访问的索引列表,配置应用访问的集群名称,Gateway根据集群名称,将请求代理到指定集群访问,如果访问的是tribenode集群,则该应用可以访问到多个集群的索引。</p>
<p>Admin服务则管控了所有的Elasticsearch集群,以及索引和集群的对应关系。一系列功能都针对多集群做了改造。</p>
<p>Sink服务已经从Elasticsearch平台分离出去,成立DSink数据投递平台,DSink Manager负责管理DSink节点,DSink Manager从Elasticsearch Admin服务获取索引的元数据信息,下发给对应的DSink节点。</p>
<p><strong>多集群架构实践总结</strong></p>
<p><strong>| 多集群架构收益</strong><br>Elasticsearch多集群架构改造给Elasticsearch平台带来了如下收益:</p>
<ul>
<li>Elasticsearch平台的隔离性可以从物理节点级别上升到Elasticsearch集群级别。对于核心的线上应用,可以使用独立的Elasticsearch集群支持。</li>
<li>不同类型的数据按集群划分,避免相互影响,减小了故障的影响面,对平台稳定性带来极大的提升。</li>
<li>Elasticsearch平台的扩展能力进一步提升,通过新增集群可以很好的做到水平扩展。</li>
<li>多集群架构最终做到了对业务方无感知,业务看起来,Elasticsearch平台就像一个无限大的Elasticsearch集群,而无需感知索引真实的集群分布。</li>
</ul>
<p><strong>| 多集群架构实践经验</strong><br>滴滴Elasticsearch平台多集群的架构已经演进了一年半时间,这期间也遇到一些多集群架构带来的挑战。<br><strong>tribenode稳定性挑战:</strong></p>
<ul>
<li>随着集群数量越来越多,前面提到的tribenode不足越来越明显,比如初始化的时间越来越长等等。我们采取的应对策略是部署多组tribenode集群,有几组连接全量的集群,互为灾备,有几组只连接核心的一些集群,用作更为重要的跨集群访问场景。</li>
<li>tribenode的ClusterState元数据包含了太多的索引和shard,Elasticsearch的search逻辑在有些case处理下容易出现耗时过长的情况。Elasticsearch在client接收到search请求时,是在netty的io线程中完成请求转发给每个shard的,低版本的Elasticsearch还没有限制一次query的shard数量,在一些复杂的模糊索引匹配shard的逻辑中,以及给每个shard发送query请求时,会出现较高的耗时,可能有超过1-2s的case,这会影响到该netty worker上的其他的请求,造成部分响应飙高的情况。我们优化了tribenode search流程中一些索引、shard膨胀之后的耗时逻辑,解决了该问题。</li>
</ul>
<p><strong>多集群配置、版本统一的挑战:</strong></p>
<ul>
<li>在只有一个集群的时候,平台只用维护一份集群的配置和版本。当集群数量增多后,不同集群间的_cluster<br> settings信息会出现部分差异,这些差异,可能会导致集群间的负载不均,恢复速度过快或者过慢等问题,每个集群还有一份基础的索引模板配置,这里面也出现了部分差异。这个问题目前我们还在解决中,我们计划将Admin服务分离成索引管理服务和集群管理服务,集群管理会专注于集群版本、配置、部署、扩容、监控等方面对Elasticsearch集群进行更全面的管控。</li>
<li>我们做的一些Elasticsearch源码优化,会先后在部分集群上线,这样导致了集群间的版本混乱的问题。我们的解决方案是在Elasticsearch和Lucene内增加内部的版本号,通过公司内部的发布系统,发布Elasticsearch的更新,后续集群管理服务会将集群的版本管理起来。</li>
</ul>
<p><strong>多集群间容量均衡的挑战:</strong></p>
<ul>
<li>我们主要从跨集群索引迁移和容量规划解决集群间容量均衡的挑战,在单Elasticsearch集群的时候,数据迁移可以依赖Elasticsearch的rebalance能力完成。在使用多集群架构后,平台内部的Elasticsearch集群会出现资源分配不均的问题,例如有些索引容量增长的很快,导致所在集群的资源紧张,有些索引数据减少,不需要占用太多资源,导致集群资源空闲。于是产生了索引跨集群迁移的需求。针对这个需求,我们通过给索引添加版本号,解决了索引跨集群迁移问题。之后我们有文章会详细的介绍该方案。</li>
<li>滴滴Elasticsearch平台实现了索引容量的自动规划,解决了集群间的容量均衡。Elasticsearch平台可以动态的规划索引的容量。当一个集群容量规划不足时,平台可以动态的迁移一部分索引到空闲的集群中。新的索引接入需求会优先接入在空闲的集群资源中。滴滴Elasticsearch平台是如何实现索引容量的自动规划,也请期待后续的分享。</li>
</ul>
<hr>
<p><strong>总结</strong><br>滴滴的多集群架构,最初是为了解决Elasticsearch单集群架构的瓶颈。为了支持多集群架构,后面的很多组件都需要考虑连接多个集群的场景,给平台架构带来了一定的复杂性。但是多Elasticsearch集群带来的稳定性和隔离性的提升,它所带来的收益远远大于架构的复杂性。改造成多集群架构后,我们扛住了Elasticsearch平台规模爆炸式增长,Elasticsearch平台的规模翻了5倍多,多集群架构很好的支撑了业务的快速发展。</p>
滴滴机器学习平台架构演进
https://segmentfault.com/a/1190000019215007
2019-05-17T16:33:17+08:00
2019-05-17T16:33:17+08:00
滴滴技术
https://segmentfault.com/u/didijishu
13
<p><strong>前言:</strong>现在很多互联网公司都有自己的机器学习平台,冠以之名虽然形形色色,但就平台所要解决的问题和技术选型基本还是大同小异。所谓大同是指大家所要处理的问题都相似,技术架构和选型也差不太多,比如都会使用 GPU 集群、采用 Spark 或 K8s 平台等。所谓小异是指各家规模不同,各家都在结合自己的情况、所处的阶段并根据自己的特点解决平台化的问题。滴滴机器学习平台的治理思路主要是:减少重复、提高效率。本文将对滴滴的机器学习平台进行全面解读,重点分享机器学习平台不同阶段所要解决的问题,以及解决问题的思路和技术方案。希望能够对大家有所帮助。</p>
<p><strong>▍机器学习平台1.0:从“作坊”向“集中化”过渡</strong></p>
<p>滴滴的机器学习平台建设开始于2016年,当时滴滴内部各算法团队逐步开展机器学习、深度学习等 AI 相关的研究和实践应用,这类算法大都属于计算密集型应用,一般都会使用单价较昂贵的 GPU 服务器。但随着业务的开展,各算法团队仅针对各自的问题做规划,导致了一种小作坊式的生产局面。</p>
<p>作坊式生产方式在早期有其积极的一面,能够保证创新的灵活性,但是越往后,这种小作坊式算法生产模式的局限就越明显:资源缺乏统筹调度,无法形成规模化效应,大量重复性工作,自拥算力有限。逐渐增多的这种小作坊式生产方式致使整体投入产出的效益大打折扣。</p>
<p>滴滴机器学习平台在这种背景下应运而生,这个阶段也主要致力于解决这些问题。这期间机器学习平台所采用的架构和技术选型主要针对作坊式生产方式的问题来展开,也就是提高复用性和规模化能力。</p>
<p>首先要解决的问题就是统一资源管理,这个“统一”要解决包括线下和线上两类问题。</p>
<p>线下“统一”的问题着重解决 GPU 的服务器选型、测试、引入、上线等的集中化。这类集中化一方面提高了服务器引入的上线质量;另一方面相比于作坊式模式,由于有 GPU 相关专业人员参与进来,GPU 的选型避免了一味追新的盲目性和发散性。</p>
<p>再者,集中化能够和公司整体大局结合起来,从而可以做最优化的选型和引入方案。</p>
<p>线上“统一”需要解决的问题细分为资源管理问题和任务调度问题,使资源使用方能够用即申请,完即释放,从而盘活整个资源大池,对平台要求则需要做到资源的隔离和管理。</p>
<p>这个阶段需要解决资源统一管理后如何避免重复性工作的问题。此时所谓的避免重复性,意在让各个算法业务不需重复诸如 Caffe、TensorFlow、PyTorch 等运行环境的构建,而是要一次构建所有用户都可用。这对平台来讲,需要做到应用环境管理、用户自定义环境、快速环境部署。</p>
<p>厘清这些需求之后,结合当时的技术环境和成熟度来看及以上的基本要求,平台选择当下盛行的 Docker 来兼做环境的管理、资源的弱隔离和任务的调度。</p>
<p>但由于此时支持 GPU 资源调度的资源管理器乏善可陈,所以我们选择对 Yarn 做了扩展以支持 GPU 资源维度上的资源管理和任务调度,环境上平台同时提供 Notebook、Jupyter 的交互接口给用户。</p>
<p>统一资源管理、环境管理后,不得不面对的问题是多个资源节点间数据共享的问题,用户在当前资源释放后申请新资源时往往对之前的数据有依赖。</p>
<p>多节点数据共享在作坊式时期受限于单个的规模,问题不会十分突出,但是集中化之后随用户增多就会逐渐尖锐起来乃至是个大的技术挑战。因为:</p>
<p>机器学习的任务计算特点依赖于 GPU 的高速计算,它们对数据访问延迟有一定要求,这要求必须有足够高的 IO 带宽做支持;<br>用户数量增加,对存储带宽的需求会变的非常大;<br>对存储系统来说,支持 POSIX 接口的要求使得现有技术方案大大减小,另外也需在高可靠性、高性能以及成本之间做折中。</p>
<p>滴滴机器学习平台在存储系统上的尝试还是借用传统超算使用的 PFS 作为整个数据存储的一级,底层网络基础设施使用高带宽的以太网络,使用 RoCE 协议做 RDMA 的支持,并往这个方向演进。</p>
<p><img src="/img/bVbsMHK?w=865&h=601" alt="图片描述" title="图片描述"></p>
<pre><code> 机器学习平台架构-Yarn
</code></pre>
<p>总的来看,这个阶段所面对的问题以内部问题为主,从作坊式到集中化生产的发展阶段,要解决的相关重复性的问题也比较简单。其中有些问题本质属于集中化后产生的问题,但是解决思路还是作坊式的,技术选型上的局限性也没有完全暴露出来。</p>
<p><strong>▍机器学习平台2.0:平台发展</strong></p>
<p>随着作坊逐渐消失,机器学习平台作为一种集中化的生产方式呈现给公司所有算法团队。平台功能开始完整和完善,监控体系,运维体系,更加精细化的资源隔离、管理及优化;根据用户不同的任务性质也提供了不同性质的任务支持。</p>
<p>经历了前一个阶段后,虽然有效降低了作坊生产的重复性工作,但也几乎必然的产生了一些新形态的重复工作。用户接入的增多,用户任务的性质也多样化,有些是实验性质的、有些是在线生产任务、有些是单卡任务、有些是多卡多机的训练任务等等。</p>
<p>每种性质的任务都有各自重复的具体形式,比如用户在模型生产后要部署模型服务就需要解决服务的 HA、负载均衡等生产服务问题,每一个在线模型都要解决这类问题。</p>
<p>再比如,用户训练时往往需要调参,而这些参数都是同形的,只是数值上的变化,这种值上的变化后就是一个个独立任务,需要用户提交任务的流程,这也是重复性的工作。</p>
<p>再比如,用户在运行多机多卡时需要参数服务器,低效的参数服务器把大量的时间浪费在通信上,这种浪费会加重用户资源使用上的重复;与这种重复形式相似的,还有模型服务要上线,为了满足服务的延迟、QPS、资源的约束,需要做从服务、到深度学习框架、再到计算库的全栈优化,基本上,大部分模型上线也需要经历这个优化过程。</p>
<p>针对上述新出现的问题,平台需要更加强大的资源管理和任务调度能力。</p>
<p>在上一时期选用作为资源管理和任务调度器的 Yarn 开始呈现出疲态,具体表现在 K8S 日臻成熟,与 Docker 的结合更加合理和完整,并能够整合多种维度的资源,使用 K8S 为解决模型服务的自动化部署提供了环境和条件,也降低了服务的运维成本。</p>
<p>综合 K8S 和 Yarn 各自的利弊,滴滴机器学习平台开始由 Yarn 架构向 K8S 建构迁移。</p>
<p><img src="/img/bVbsMJ1?w=865&h=558" alt="图片描述" title="图片描述"></p>
<pre><code> 机器学习平台架构-K8S
</code></pre>
<p>针对用户同形调参的效率问题,平台对用户的 Python 代码做语义分析以自动识别出哪些参数可能会是需要调整的参数,用户只需要设置值域和步距就可以自动获取整套参数的模型训练任务以及最终的结果。</p>
<p>针对多机多卡训练效率问题,平台结合自己的硬件特点和通信模式特点,开发了滴滴参数服务器。滴滴参数服务器采取环状结构,实现了高效的 RDMA 通信的 Allreduce 算法。</p>
<p>环状结构而非中心集中的 server-client 模式,消除了网络传输可能的带宽竞争和网络拥塞。底层自研的高效 RDMA 通信库,规避了设备厂家提供用户态 Verbs 内部分性能损失,重写的底层通信库实现了 sig/read 及 post/recv 两种模式,尽量规避了 RDMA 固有的通信开销,充分挖掘了硬件的属性来提高性能。</p>
<p>另外,自研的 Allreduce 算法巧妙重叠了计算和传输,尽量减少了不必要的内存拷贝来减少额外代价,并充分考虑了 GPU 拓扑、CPU 亲和性等硬件属性来提高性能。</p>
<p>在机房 40G 带宽的 RoCE v2 RDMA 网络实际测试中,对比业界的 OpenMPI 和 Nvidia 的 NCCL2 方案,滴滴参数服务器有明显优势。</p>
<p><img src="/img/bVbsMKK?w=865&h=291" alt="图片描述" title="图片描述"></p>
<p>针对模型服务部署和优化,平台结合自己的场景特点开发了 DDL(DiDi Deep Learning) Serving 服务框架、IFX 框架和 Autotuning 优化库,极大加速了模型上线部署和优化过程。</p>
<p>针对模型服务部署和优化,平台结合自己的场景特点开发了 DDL(DiDi Deep Learning) Serving 服务框架、IFX 框架和 Autotuning 优化库,极大加速了模型上线部署和优化过程。</p>
<p>DDL Serving 独创自适应的 batch 机制,优化 RPC 协议,解决 Tensorflow Serving 的缺陷,相比于 Tensorflow Serving 性能对比加速如下:</p>
<p><img src="/img/bVbsMKY?w=777&h=529" alt="图片描述" title="图片描述"></p>
<p><img src="/img/bVbsMK8?w=729&h=529" alt="图片描述" title="图片描述"></p>
<p>DDL Serving 框架服务本身不再成为整个服务链路中的瓶颈点,对于一些轻量模型可以有 3 倍的性能提升,包括 RT 和 QPS 的提升, 而对于一般模型,性能热点落在深度学习框架层。</p>
<p>因此,针对框架层,我们自主研发了深度学习框架 IFX,并同时适配于 GPU 服务器和移动端平台。在 GPU 服务器上,由于 CUDA 存在 context 管理的问题,所以我们设计实现了一种 GPU 上的并发机制,有效地绕开了这些问题所带来的额外开销,另外对大量的 OP 做了优化,使得 IFX 的性能远高于 Tensoflow 乃至 TensorRT。</p>
<p>IFX 针对移动端的不同硬件配置,比如:流水线长度、顺序乱序、超标量等特点进行指令重排、访存优化,结合业务的计算特点,使得 IFX 的性能取得不俗的表现:</p>
<p><img src="/img/bVbsMLo?w=865&h=213" alt="图片描述" title="图片描述"></p>
<p>在 IFX 的优化过程中,大量的重复工作基本在 Tuning Blas 计算,由于硬件架构不同,不同模型的计算量、计算访存比、计算访存模式都不同,在极高性能要求下都需要综合这些具体的情况做针对性的优化。这些优化都很底层,并且调优都相对繁琐,对于上层服务用户来讲,不必关心这些底层细节。</p>
<p>为解决这类问题,平台开发了 Autotuning 工具链,包括 Kepler、Pascal、Volta 架构的原生汇编器。</p>
<p>对于用户来讲,只需要把 GPU 上的二进制代码发给平台,平台就可产生在该 GPU 平台上几乎是最优,也就是当前最高性能优化后的二进制代码。</p>
<p>滴滴机器学习平台团队也是目前除了 NV 以外,自己掌握 NV GPU 原生汇编器支持版本最多,对 NV GPU 微架构最了解的。</p>
<p><img src="/img/bVbsMMa?w=865&h=123" alt="图片描述" title="图片描述"></p>
<p>这些“重复问题”随着集中化和平台化产生,也在平台化的环境下使得解决这些“重复”变得有意义。</p>
<p>集中化、平台化带来的第二个好处便是在此基础上,通用性的需求逐渐会沉淀为平台的服务。</p>
<p>比如,相似检索的需求在滴滴地图的 POI 优化、人脸检索、视频图像内容检索等业务场景中都是共性需求,因此平台会获得足够的业务信息来开发这种平台级的服务,而在作坊式时代很难获得这类跨业务场景的需求而自发的沉淀出平台服务,大多还是自扫门前雪。</p>
<p><strong>▍机器学习平台2.1:内外云平台成形</strong></p>
<p>集中化生产后的第二个影响,随着平台能力的增加以及孵化落地算法逐步丰富,加上滴滴内部数据、AI 工程和算法逐步积累成熟,机器学习平台的功能、定位也变得多样化。</p>
<p>除了服务好滴滴内部机器学习平台用户,进一步夯实资源调度、任务管理、监控运维等能力外,平台开始承接内部能力对外输出的职能,期间机器学习平台和滴滴云着手在公有云上打造从底层资源到上层平台、从公有云到私有云的解决方案。</p>
<p>机器学习内部的集中化生产也给滴滴机器学习平台能力的输出做了储备,但外部客户的技术产品要求相对更复杂。</p>
<p>这种复杂首先体现在产品要求的多层次性:有对资源乃至对硬件的直接要求、有对具体服务的需求、也有例如在私有云中对平台能力的需求;其次, 产品考量因素的多维性:资源的性价比往往只是一方面,安全性、稳定性、与其他基础设施的整合能力等也都是影响用户决策的因素;最后,横向各友商竞品的对比。</p>
<p>所有这些问题都是滴滴机器学习平台对外服务碰到的问题,但是这些问题不可能做到“毕其功于一役”,都是分阶段分步骤,有侧重的解决此间的问题。</p>
<p>第一步要解决的是基础问题,如何透出能力,如何保证客户的安全性,如何在前两个能力的基础上,尽最大力减少外部用户的重复性工作(用户使用的成本)和滴滴机器学习平台的重复性工作(产品性价比)。</p>
<p><strong>▍GPU 资源:减少资源的重复性工作</strong></p>
<p>相比于内部的用户,外部用户使用资源需要有一个安全的隔离环境,仅用 Docker 的弱隔离方式无法给用户提供安全且隔离的环境。所以滴滴云上 GPU 云资源使用 KVM 和 GPU 透传的方式把 GPU 资源透传给用户。</p>
<p>滴滴机器学习平台技术团队对 GPU 的使用颇有心得,团队成员也是早期一批在工业界尝试 GPU 的团队,积累了丰富的 GPU 使用一线的知识和经验,而且这些在滴滴内部被佐证十分有效,从 GPU 资源、拓扑和相关配套上都特别花心思,所以相同 GPU 型号,用户往往可以获得更好的性能,对比如下图。这部分的沉淀也减少了外部用户在探索使用 GPU 过程中的重复性工作,降低了使用的隐性成本。</p>
<p><img src="/img/bVbsMMz?w=865&h=531" alt="图片描述" title="图片描述"></p>
<p><strong>▍弹性推理服务(EIS):减少服务部署优化的重复</strong></p>
<p>所有的算法模型最终都需要用于生产服务,国外有很多 PAML 平台能够部署机器学习模型服务,机器学习平台在滴滴云上也提供了一种模型部署服务——EIS(弹性预测服务)。</p>
<p>EIS 服务根植于内部使用的 DDL Serving 服务,但因在云上服务我们对一些理念的坚持,所以大家可能会产生我们有“起大早赶晚集”的疑问。</p>
<p>实际上,EIS 在滴滴内部以 DDL 的形式出现的相对不算晚,这一块的服务市场现在只能说是刚刚起步,产品的差异化和多样化会是必然的趋势,对用户来讲也有更好更大的选择空间。</p>
<p>目前,市面上大大小小提供 PA 服务的厂商大都有各自的特点,但总的来说他们对这个产品的定位依然仅仅是作为资源产品的辅助角色,着重为用户解决资源和部署问题。这种辅助角色,有他的好处,主要包括:</p>
<p>模式简单,把服务转化为最小粒度资源开销,按最小单位资源消耗来计费;<br>对基础设施的能力要求降低,简化为资源开销,本质上只是多了一种资源的售卖形式;<br>服务厂商的工作最小化,虽然用户可以选择多种资源,并且每种资源的都有各自理论上的计算能力,用户怎么利用好这些资源是用户自己的事情。</p>
<p>这个模式的问题在于服务商虽然为客户解决了一部分问题,但是对用户实际的服务部署考虑仍然不周。为什么?</p>
<p>原因在 DDL 描述中也提到过,模型服务部署服务都需要用户自己优化服务以满足 RT、QPS 的要求,更进一步说,成本如何最优化,用户使用云服务,成本几乎是必然会面对和慎重考虑的。</p>
<p>所以从这个点来看,PA 服务提供商以资源为主,服务为辅的模式的缺点也显而易见:</p>
<p>最小粒度资源的粒度对模型服务来说,粒度依旧比较粗,如若使用到 GPU,问题更加突出;</p>
<p>资源的理论计算能力对用户来讲往往仅是个理论数字,受限于硬件的限制和客户自己的技术能力,客户往往并不能充分利用 PA 厂商提供的资源的计算能力,而一般利用率都有限,这实际使用和标称的理论数字之间的资源费用实际是由用户买单的,而更甚者,对用户来讲这里有两部分工作是重复的:资源的使用优化的重复,服务部署的运维相关工作的重复。</p>
<p>根据我们内部用户和一些外部用户的经验,服务最核心的技术指标是 QPS 和 RT,进而才是满足这两个指标情况下的部署成本和使用成本。而这些成本的降低则必须在尽可能减少用户的重复工作和“实用实销”的基础上,除了一般服务部署需要的 HA 和运维支持外,EIS 从技术架构设计上侧重于解决这两方面问题。</p>
<p>从 RT 来讲:用户服务 RT 的开销受限于网络链路和实际前向计算的开销,为了减少网络链路的开销,滴滴云花了不少时间,在公有云上实现了纯公有云化的 Gateway,一方面用于支持用户自定义的鉴权等操作,另一方面也最小化网路跳数以降低网络的开销,保证用户服务的 RT。</p>
<p>从 QPS 来讲,EIS 使用滴滴机器学习平台的 DDL Serving 作为服务引擎框架,使用 DDL Serving 的用户可以忽略底层硬件的细节,从而可以避免用户重复地去做服务框架层面的已知的优化工作,这样也为实现用户“实用实销”提供了条件。可以通过以下的架构图了解:</p>
<p><img src="/img/bVbsMMT?w=865&h=503" alt="图片描述" title="图片描述"></p>
<p>要做到“实用实销”,还有一个非常关键的环节就是需要知道用户的模型实际的计算需求量,以及某一种硬件下的计算利用率。</p>
<p>我们开发了一个自动压测模块,用户提供模型和部署输入就可以获得使用 DDL Serving 在某种硬件下的计算性能,进一步回归出某种 RT 性能要求下的 QPS 能力。</p>
<p>对用户来讲,用户折算出业务需总的 QPS 后按 QPS 横向扩容即可,相当于用户只负担了实际消耗的计算性能的那部分资源,这比之前的模式是更加细粒度的资源控制。</p>
<p>用户优化上的重复性工作的减少,如之前讲过的除了服务框架的优化外,还有一部分优化是花在计算性能的优化上,但计算性能的优化往往取决于程序的计算特性和相关的硬件特性,并且每种模型都有各自的特点。</p>
<p>这部分工作 EIS 也提供了 Autotuning 的优化服务,用户需要提供他的二进制代码,通过 Autotuning 服务后会产生某种模型和框架下在指定硬件下几乎是最优的性能代码。</p>
<p>Autotuning 服务除了能降低重复基础的和琐碎的优化工作外,也能够提升用户模型服务 RT 和每 QPS 实际资源消耗资源。</p>
<p>目前 EIS 已经接入滴滴内部大量的业务,其整个功能模块图如下。因为一些限制,对外部客户,当前滴滴云 EIS 服务还是通过提交工单接入的方法,用户自助的方式马上会上线。</p>
<p><img src="/img/bVbsMM1?w=865&h=354" alt="图片描述" title="图片描述"></p>
<p><strong>▍简枢:降低用户重复平台建设</strong></p>
<p>同 EIS 一样,机器学习平台级产品在内部积累了丰富的一线的平台经验,基于此,机器学习平台在滴滴云上开发了平台级产品简枢。</p>
<p>简枢包装了多种平台能力,弱隔离方案的资源管理、多种任务管理、监控报警、在线服务快速部署等,能够帮助其他公司在平台化过程中少踩坑,快速具备平台能力,提高生产效益。</p>
<p><img src="/img/bVbsMNg?w=865&h=355" alt="图片描述" title="图片描述"></p>
<p><strong>▍未来展望</strong></p>
<p>对于机器学习来讲,计算力仍然是最具革命性的力量,正如 2011 年开始的这波深度学习浪潮的助力正是 GPU 一样,未来计算力还是工程层面的制约力。</p>
<p>如 Jeff Dean 所言“事实证明,我们真正需要的是超过现在 100 万倍的计算能力,而不仅仅是几十倍的增长。”因此,对平台来讲,如何更好的管理不断爆发式增加的计算力、如何有效的释放出这些计算力,如何驾驭好这些计算力仍然需要平台不断的探索、实践、技术升级等等。</p>
<p>所有平台的生命力源自于生产效率的综合提高,降低整体成本。对于滴滴机器学习平台而言,内部第一目标是要降低滴滴在使用最新的机器学习、深度学习、强化学习等技术上能够保证整体效率和成本控制,同时兼顾创新的活力。</p>
<p>对于外部而言,秉承持续为客户创造价值的理念,深化云平台产品的各项产品功能、质量和成本,为客户打造物美价廉的技术产品。</p>
<p><img src="/img/bVbsMNy?w=865&h=298" alt="图片描述" title="图片描述"></p>
<pre><code> 机器学习平台3.0
</code></pre>
<p>具体来说,滴滴机器学习平台要实现 3.0 阶段,也即从硬件选型到基础设施到上层整个软件栈,能够做到内外统一架构,降低内外两部分的重复性工作。</p>
<p>同时,我们会从 AI 解决问题的效率和规模两方面着手,在平台上提供更丰富的功能,比如开发算法市场、模型市场、数据市场、GUI 界面以提高用户使用各种学习技术的效率,也会继续沉淀更多的具体服务,比如:人脸比对、语音识别、翻译等等。</p>
<p>如果您对滴滴云GPU云主机、弹性推理服务(EIS)、机器学习平台等产品、技术解决方案感兴趣,欢迎访问<a href="https://link.segmentfault.com/?enc=Co5HsM7AbLpjfyQS%2FdsGUQ%3D%3D.l%2FUlaCO6AT%2FcB%2BU8bgVSVvlUHIA8fr0jBy2eP6djer8OlUEXRfcrPdug6skJixX3" rel="nofollow">滴滴云官网</a>。</p>
<p><img src="/img/bVbsMNX?w=1280&h=370" alt="图片描述" title="图片描述"></p>