谷歌实习体验

  • 2022/7/8 21:12:00

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

背景

之前发表在 OSDI 2016 以上叫 Slicer。谷歌产品的日常使用是我们组和我们兄弟组的系统。之前发表在 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

与我联系