谷歌实习体验

  • 2022/1/21 23:52:00

上周五,我为期13周的谷歌实习结束了,因为我甚至没有照片名称。我给谷歌写了7500行C 代码,再加上还没有合并的1000行,我觉得我对自己贡献代码的数量和质量还是比较满意的。这次谷歌实习是我第一次在工业界的经历,感觉收获很多。

背景

我在谷歌的职位是软件工程实习生(Software Engineering Intern),工作内容是编写代码。我的团队在谷歌内部制作了一个事件通知系统,之前发表在 SOSP 2011 以上叫 Thialfi。我实际上是在向我们兄弟组的一个项目解释代码。我们的兄弟组做的是谷歌内部使用的自动分片和负载平衡系统,之前发表在 OSDI 2016 以上叫 Slicer。我们组和我们兄弟组的系统用于谷歌产品的日常使用。

感想

下面我就简单列出一下我在谷歌实习时感触很深的地方,大部分都是技术性的。

设计文档:在我开始编写代码之前的两周,我先按要求编写设计文档(Design Docs),然后我和小组里的人讨论了文档。设计文档大致介绍了计划添加新产品或新功能的动机,提供了什么功能,如何设计系统,需要修改现有接口,提供什么新接口,小细节上有什么复杂点,以及不同替代方案的优缺点。我认为写设计文档是一件非常好的事情,它可以帮助你清楚地思考一些细节,而不是在编写代码时开始挣扎。此外,在小组讨论后,我们可以进一步发现一些问题,但也可以让每个人对这个新功能有一致的理解。当然,设计文档也有缺点。一是在早期阶段不可能清楚地考虑所有的地方,后期的实现总是与原始设计文档不同。我认为与实际实现脱钩的设计文档失去了意义,所以我们需要在后期修改设计文档,这带来了额外的维护成本。但总的来说,我认为设计文档在后期仍然很有用。当我不记得小细节时。您可以参考此设计文档。对于小组外的人来说,阅读设计文档可以从大方向了解项目的设计。注释是文档:每个头文件的前面都有很长的代码注释来介绍这个类别(Class)它是用来做什么的,有些甚至包含示例。每种公共方法(Public Method)还有详细的注释。大多数图书馆没有特殊的文档,文档写在第一个文件中。使用代码搜索实际上非常舒适。单个大代码仓库:几乎整个谷歌代码都在同一个大代码仓库中,不同的项目放在不同的文件夹中。我认为这带来的一个很大的好处是构建工具(Build System)很容易找到文件和符号,反映在使用代码搜索和代码自动完成非常准确。代码仓库没有分支,提交的代码直接合并到线性主线。提交代码的过程实际上非常简单和粗糙。首先是创建一个修改列表(Change List,CL),每个CL有一个基准版本。CL代码仓库会检查基准版本后是否有其他东西CL修改与当前相同的文件,只要有,就不允许提交。此时需要提交CL同步到主线的最新版本,解决冲突,然后再次尝试提交。没有分支的问题之一是确保每次提交CL没有大问题:至少可以编译,不影响现有的正常功能。否则,所有依赖您的项目的项目都将出现问题。谷歌减少这个问题的方法是:代码审查和所有影响测试都必须通过。相比之下,我认为它可能是 Github Flow 很像。每开一个CL开一个新的分支(Branch)。可以随时给谷歌CL做快照(snaphost),相当于 git commit。编写代码后,应发送给他人审查,相当于发送合并请求(Pull Request)。如果有冲突的话,一样都是变基(Rebase)在最新的主干上。经过讨论,大家都很开心(LGTM Approved),另外,如果试验运行,可以合并到主线,相当于合并请求(Merged),然后分支被删除。分布式文件系统:所有代码和生成的文件,包括目标文件(Object File)、生成代码(Generated Source Code)、可执行文件(Executable File),都放在分布式文件系统上。这意味着所有文件都有权威版本(Canonical Version),也就是说,你在办公室里写了一会儿代码,在服务器上写了一会儿代码,在笔记本上写了一会儿代码,在家里写了一会儿代码。事实上,它是相同的代码。你不必担心更新哪个代码。此外,谷歌还可以开发各种周边开发工具。例如,云IDE,浏览器上的云IDE更改一行代码后,当地编辑器可以立即看到修改;另一方面,办公电脑上更改了一行代码,笔记本打开了云IDE你可以马上看到它。此外,服务器上还可以进行编译和操作测试,因为服务器上看到的代码与本地开发计算机上看到的代码相同。所以使用云IDE也可以编译和操作。构建系统(Build System):谷歌已经开源了他们的构建系统 Bazel,我认为这是一个非常先进的建筑体系。在进入谷歌实习之前,我一直认为 Bazel 太麻烦了,每个目标(Target)每一个依赖的目标都应该写下来,目标的名称很长,包含完整的路径,所有的目标都需要在工作空间(Workspace)在指定的文件夹树中。当谷歌编写代码时,我明白为什么它是这样设计的,因为谷歌使用一个单一的代码仓库,所有的代码都在一个文件夹树中,所以每个目标自然都带来了文件的路径。至于依赖性,可以自动填写工具,但没有开源。每个目标都可以指定可见性,其他项目可以使用什么目标。例如,内部测试的类别可以被隐藏起来,这样其他项目就不能使用了。这相当于提供了一份合同,公共目标在维护时应该更加小心;内部目标可以改变,因为它不会影响其他项目。如果你错过了依赖性,它会提示你找不到第一个文件,并强迫你添加依赖性。我认为这个设计非常巧妙,可以用来确保所有代码中使用的第一个文件对应的目标都在当前目标的依赖列表中。为什么要维护依赖性?我认为,因为,一旦我们能够确保依赖列表是准确的,每当文件发生变化时,就很容易计算出影响了什么目标。这样,提交CL当你运行受影响的目标测试时,你只需要进行测试。系统建设背后是一个用于编译和测试的集群。编译和测试很容易并行,所以编译和测试只需要一台无脑堆叠机器就可以非常快。谷歌里随便写一个 Hello World 都要编译上千个文件,我所在的项目虽然本身只有几十个文件,但是要编译6万多个文件。但即使如此,完整地从头开始编译也只需要一两分钟时间。测试也可以并行跑。Bazel 可以指定每个测试可以分成几个平行的目标(shard_count),如果有测试的话(Test)有很多测试用例跑得很慢。(Testcase),它们可以平行运行,可以大大降低测试的总时间,而不是顺序运行。这特别是在集成测试中(Integration Test)很有用。搜索后,我发现 googletest 有这两个环境变量GTEST_TOTAL_SHARDS / GTEST_SHARD_INDEX,但是文档里没有写。我猜没有 Bazel 也可以直接操直接操纵这两个环境变量并行运行测试,那就麻烦多了。测试不仅可以平行运行,还可以重复运行。这在编写多线程序时非常有用,因为有时不确定程序中是否会有一些并发错误(Concurrency Bug)。这个时候可以多跑几次测试来增强信心。Bazel 里面加个 --runs_per_test 就能够指定把每个测试重复跑若干次。因为在谷歌里面堆了好多服务器来跑编译和测试,所以说如果一个测试跑一次要一分钟,把这个测试跑上1000次也只要一分钟。所以我经常就把一些我觉得不是很确定的测试跑个一万次。另外,要跑 Sanitizers 也很方便,见下面。可退回的调试器(Reverse Debugger):云IDE如何调试上述程序?首先记录程序的执行过程,然后回放(Record and Replay)。逆向调试器(Reverse Debugger)其实思路很简单,程序执行过程中的不确定性(Non-deterministic)只要把指令写下来。当这些指令回放时,将结果替换为当时记录的结果。但它必须非常复杂。一定有很多脏活(Dirty Work)。我以前听说过 rr,但从未使用过 rr 或其他类似的工具。这一次,我在谷歌采访了谷歌内部实现的反向调试器,并用它发现了我代码中的一两个错误,但总的来说,它并不是特别容易使用。一个是跑得很慢;另一个是用户界面(UI)它不是特别容易使用,特别是当不同的线程切换和同一行代码多次执行时。但总的来说,看到逆向调试器仍然非常兴奋,尤其是云IDE我觉得结合是个好主意。IDE:前面提到了云IDE嗯,我觉得其实很好用。代码补充(Auto-complete)又快又准。实习之初,我们整个小组都在用CLion,后面的每个人都被云取代了IDE因为受不了,CLion总是卡住,即使打字也会卡住。代码搜索:谷歌内部的代码搜索工具真的很容易使用。您不仅可以搜索字符串和正则表达式,还可以搜索准确的符号;你不仅可以跳到声明和定义,还可以找到所有使用的地方;而且非常快,几乎是实时结果。GitHub 也可以添加此功能,但注定要困难,因为每个项目的构建系统不同,很难准确识别符号。我以前用过几天 Sourcegraph 浏览器插件,感觉经常找不到符号。积木分布式系统:谷歌有太多优秀的分布式系统,尤其是 Spanner。如果一个项目愿意假设 Spanner 非常可靠,性能好,几乎免费获得高可靠性(High Availability)、地域 ** (Geo-replication),而且有良好的定义(Well-defined)的全局时钟(Global Wall Clock)。至于其他一些需求,其他项目往往会解决棘手的问题,例如,我们小组的项目可以提供事件通知,以确保缓存的新鲜度(Cache Freshness)。因此,在谷歌建立分布式系统有点像积木。但事实上,我们仍然会考虑尽量避免故障扩展(Escalation),也就是说 Spanner 如果出了问题,不能全部使用 Spanner 系统变得不可用。C :写谷歌C 真的很舒服,因为有各种各样的库可以使用,开源 Abseil 就是其中的一部分。C 文档也很多,包括公开 Tips of the Week 和 Style Guide,我认为这些文档是为了提升C 水平,写得更容易维护C 代码很有帮助。静态线程安全分析(Static Thread Safety Analysis):这是我第一次知道原来 Clang 和 GCC 在编译过程中支持检查互斥锁(Mutex)是否被锁定或释放。我以前写代码的时候觉得,如果把每个函数对互斥锁的要求写在函数开头的注释上(比如我写的代码),然后在编写代码时遵守此协议可以减少许多低级错误,如忘记锁或锁。出乎意料的是,编译器直接支持在编译过程中这样做。强烈推荐C 程序员使用此功能。Sanitizers:另一中使用的另一个非常有用的工具是 TSan / ASan / MSan,可用于检查多线程和内存相关错误。并配合 Blaze 使用非常简单,只要添加 --config=tsan 就行了。网上也有人把这个配置搬到了 Bazel 上面。单元测试:我认为这次实习最大的提高是如何编写可测试的代码(Testable Code)。在我完成设计文档后,我只花了三天时间写了1000多行代码,可能完成了我想要实现的功能的简化版本。然而,在完成编译错误后,我很愚蠢,因为我不知道如何操作我的代码,也不知道如何测试我写的代码。我唯一能想到的就是端到端测试(End-to-end Test)。后来在我的主管(Host)的耐心教导下,终于一步一步把这些代码重构(Refactor)了,花了一个多月的时间,代码也膨胀到了好几千行。我觉得总体上来说要让代码可以测试,就要尽可能地把代码拆成各个小的类,然后就可以测试一下每个类的公有方法,再测试一下类与类之间的交互。这样一来对代码的信心就会增强很多。依赖注入(Dependency Injection)我觉得是一个非常好用的技巧。写单元测试的时候我们希望尽量避免跟外部的系统进行交互(比方说网络、RPC、数据库、文件系统),或者是想要模拟这些外部系统返回错误结果。在这样的情况下,就可以首先规定一个接口(Inte ** ce),写一个抽象类(Abstract Class),然后让具体的实现继承这个抽象类,再为测试写一个假的对象(Fake Object)或者模拟对象(Mock Object)也继承这个抽象类。调用者手上就拿着一个抽象类的指针就行了。测试的时候用假的对象,就可以模拟各种错误的情况,也不用真正地对外界产生副作用。有意思的是,我们组和我们兄弟组极力反对模拟对象(Mock Object),力推假的对象(Fake Object)。我觉得是有一些道理的,模拟对象与实现的细节依赖太大了,有时候稍微改一下接口整个测试代码就要大改。还有一个原因是 gMock 声明式的语言不是很好用,而且一定要在测试的开头把所有的行为都指定好,这让测试代码看起来非常的零乱。代码覆盖(Code Coverage):在不知道要写什么单元测试的时候,看一眼代码覆盖可以找到那些没被测试到的情况。不过代码覆盖只能起到辅助作用,哪怕做到了100%覆盖也不一定代表程序是正确的,因为可能错误是在更高层次的地方。

也有一些非技术方面的感想。

透明的人事管理结构:可以查到任何员工的工作信息,包括照片、工位、所在部门和项目、从直属主管(Manager)到CEO的人事管理链。吃自家狗粮(Dogfooding):谷歌内部用到了非常多自己家的产品。比方说文档、表格、幻灯片全都用的是 Google Docs;开会全都是用 Hangouts;Calendar 还可以预定会议室;许多工程师选择使用 Chromebook 工作。我有幸也拿到了一台 Pixelbook,感觉其实非常好。3000x2000分辨率的高分辨率屏幕看起来很舒服,键盘软软的敲起来也很爽,整机非常的轻。因为有云IDE,所以正常的工作完全没有问题。也能够外接显示器。不过坏处就是系统经常崩溃自动重启;Pixelbook Pen 体验很差,比起 Apple Pencil 基本上就是不可用,而且除了一个特殊版本的 Google Keep 以外也没有应用支持。在谷歌实习的时候每天一杯卡布奇诺(Cappuccino),喝了一个月之后咖啡师就记住我的脸了,直接就知道我要喝什么了。现在回过头来喝星巴克的卡布奇诺,感觉是真的难喝(不过星巴克的非咖啡饮品还是不错的呀)。Kirkland 的食堂实在是难吃,不过有免费的吃的就很好了。谷歌实习体验

立即行动,开启 海外推广 精准营销之旅

请联系您的营销顾问,获取定制报价单、客户案例及行业分析报告。

运营中心:
东莞 / 深圳 / 广州 / 上海 / 杭州 / 宁波 / 温州 / 西安 / 武汉

全国免费咨询热线: 0755-27908682

本站内容部分源于网络,若有侵权,请联系我们会第一时间删除,谢谢!

18664972870

与我联系