16 开放问题

1 为什么人们讨厌改变?

人们对改变的抵触是一种自然的心理反应,这种现象在心理学和行为经济学中都有深入的研究。以下是人们讨厌改变的几个主要原因:

1. 对未知的恐惧

改变往往意味着进入未知领域。人类大脑在进化过程中发展出了对未知事物的警惕性,因为在原始环境中,未知往往意味着潜在的危险。这种本能延续到现代社会,表现为对改变的不安和抗拒。

2. 损失厌恶(Loss Aversion)

诺贝尔奖得主Daniel Kahneman的研究表明,人们对损失的敏感度是对同等收益敏感度的两倍左右。当面临改变时,人们倾向于关注可能失去什么,而非可能获得什么,这导致了对改变的抵触。

3. 舒适区效应

人们习惯于在熟悉的环境和模式中操作。舒适区提供了安全感和确定性。走出舒适区需要额外的认知努力和情感投入,这对大脑来说是"昂贵"的。

4. 沉没成本谬误

人们往往因为已经在现有系统、流程或技能上投入了大量时间和精力,而不愿意放弃它们。即使改变能带来更好的结果,沉没成本也会让人难以做出改变的决定。

5. 社会和文化因素

  • 群体压力:如果团队中大多数人抵制改变,个人很难独自接受改变

  • 组织惯性:成熟的组织往往有固定的流程和层级结构,这些都会阻碍改变

  • 传统和习惯:文化传统强化了"以前就是这样做的"的思维模式

6. 能力担忧

改变可能要求学习新技能或适应新方式。人们担心自己无法胜任新的要求,或者害怕在学习过程中显得无能。

如何帮助人们接受改变?

  1. 清晰沟通:解释改变的原因和预期收益

  2. 渐进式推进:将大的改变分解为小步骤

  3. 提供支持:培训、资源和情感支持

  4. 展示成功案例:让人们看到改变的正面效果

  5. 让员工参与:在改变过程中征求他们的意见

  6. 承认困难:认可改变带来的挑战和不适

2 向你的奶奶解释线程

每台计算机设备都会有一个计算芯片,这个芯片负责所有设备的运行,我们可以将计算芯片当作一个人,比如说奶奶你。

退休后生活典型的一天是这样的,早上起床要给家里人准备早餐,为小孙子准备上学的书包,把昨天的衣服放到洗衣机洗一下,最后还要约"老"伙伴去哪个超市购买便宜的蔬菜。那么你该怎么将这些事情处理的紧紧有条呢?

最直接的方法是将这些事情一个个完成,比如先一直待在厨房做早餐,做完之后去书房把小孙子的昨天的作业收拾完毕,然后整理好书包;紧接着去卫生间把昨天的换下来的衣服收集起来然后放到洗衣机中清理,等衣服洗好晾干,最后在微信群中问一下具体去哪个超市。这样做虽然可以非常直截了当地将这些事情处理完毕,但是带来了一些问题,比如说孙子今天要不在家吃早饭,那么当你还没有做完早餐的时候,孙子就已经开始抱怨为什么书包还没有整理好。在你洗衣服的时候,微信群已经在疯狂 @ 你了,她们想知道你到底今天会不会去。从一个角度来看的话,这段时间你每次只能做一件事,因为你只是一个人,没法做到一心两用。早期的计算机芯片就是这样的,每次只能操作一件事。

之后人们就提出了线程这个概念,继续以奶奶的一天来举例,我们将奶奶的的时间划分为若干个时间片段,每次在做一件事一段时间后,就去做另外一个事情,比如在做早饭的时候,在烧水的时候,去收拾一下孙子的书包;收拾完书包去厨房继续煎个鸡蛋,完毕后去微信群里回复一下消息。然后将书包放到门口,然后去卫生间收拾衣服放入洗衣机,最后将早餐放到餐桌上。如果说时间切换的再细一点,看上去好像奶奶有三头六臂,同时把这些事情全部完成。

我们将每件事当作一个任务,执行这个任务叫做线程。操作系统会把处理器划分为若干个时间片段,每个片段都会执行一个线程。虽然我们只有一个处理器,但是如果我们的时间片段足够小,那么我们从宏观上看这些线程是同步执行的,这也是我们为什么要设计出线程。

3 作为一个软件工程师,你想要既要有创新力,又要产出具有可预测性。采用什么策略才能使这两个目标可以共存呢?

创新力和可预测性看似矛盾,但通过合理的策略和流程设计,两者可以和谐共存。关键在于在正确的阶段采用正确的方法。

分离探索和执行阶段

探索阶段(创新优先)

  • 使用时间盒(Time-boxing)进行技术调研和原型开发

  • 允许失败和快速迭代

  • 采用Spike(技术探针)来验证想法

  • 进行头脑风暴和设计冲刺(Design Sprint)

执行阶段(可预测性优先)

  • 使用成熟的估算方法(如故事点、历史数据参考)

  • 采用经过验证的技术和模式

  • 建立清晰的里程碑和交付物

  • 实施持续集成/持续部署确保质量

具体策略

1. 20%创新时间 仿照Google的做法,分配固定比例的时间用于探索性工作:

  • 这部分时间不纳入常规交付计划

  • 产出可能成为未来产品功能的种子

  • 员工可以追求自己感兴趣的技术方向

2. 创新管道(Innovation Pipeline) 建立从想法到产品的渐进式流程:

3. 双轨敏捷(Dual-Track Agile)

  • 发现轨道:产品发现、用户研究、实验

  • 交付轨道:构建经过验证的功能

4. 技术雷达和实验

  • 维护技术雷达,追踪新兴技术

  • 在非关键项目中试验新技术

  • 成功验证后再引入主要项目

5. 模块化架构

  • 设计松耦合的系统架构

  • 允许在局部进行创新,不影响整体稳定性

  • 使用特性开关(Feature Flags)控制新功能发布

6. 风险分散

  • 每个迭代包含一小部分创新工作

  • 主要任务仍是可预测的交付

  • 创新任务的失败不影响核心交付

平衡的关键

方面
创新
可预测性

目标

发现新可能

按时交付

方法

实验、探索

规划、执行

失败态度

接受失败

规避风险

时间

弹性

固定

度量

学习成果

交付指标

成功的关键在于:明确区分两种工作模式,在组织层面建立对两者的支持和理解,让团队知道何时应该创新、何时应该专注交付。

4 什么让好代码变得更好?

好代码已经满足了基本的功能需求,而让好代码变得更好,需要在多个维度上精进。以下是让好代码升华为优秀代码的关键因素:

1. 可读性和清晰度

命名的艺术

  • 变量名、函数名、类名都应该清晰表达其意图

  • 避免缩写(除非是广泛认可的约定)

  • 名称应该回答"这是什么"或"这做什么"

结构清晰

  • 函数应该短小精悍,只做一件事

  • 代码的逻辑流程应该一目了然

  • 适当使用空行和代码块来组织逻辑

2. 简单性

"简单是可靠的先决条件" — Edsger Dijkstra

  • 避免过度设计和不必要的抽象

  • 选择最直接的解决方案

  • 删除所有不必要的代码

3. 可测试性

  • 函数应该是纯函数(相同输入总是产生相同输出)

  • 依赖应该可以被注入和替换

  • 代码应该易于进行单元测试

4. 健壮性

  • 优雅地处理边界情况和异常

  • 输入验证完善

  • 失败时给出有意义的错误信息

5. 性能意识

  • 选择合适的数据结构和算法

  • 避免不必要的计算和内存分配

  • 理解性能关键路径

6. 一致性

  • 遵循团队/项目的编码规范

  • 相似的问题用相似的方式解决

  • API设计保持一致性

7. 自文档化

优秀的代码应该能够自我解释:

8. 可演化性

  • 预留扩展点,但不过度设计

  • 遵循开闭原则:对扩展开放,对修改关闭

  • 解耦组件,降低变更成本

总结

让好代码变得更好的核心思想是:为阅读者而写代码。代码被阅读的次数远多于被编写的次数,优秀的代码应该让下一个阅读它的人(包括未来的自己)能够快速理解、放心修改。

5 解析什么是流,并且实现它;

流(Stream)是一种抽象概念,用于表示数据的序列化传输。它允许我们以连续的方式处理数据,而不需要一次性将所有数据加载到内存中。

流的核心概念

  1. 顺序访问:数据按顺序读取或写入

  2. 惰性求值:数据在需要时才被处理

  3. 无界性:流可以是无限的(如网络数据流)

  4. 一次性消费:流通常只能遍历一次

流的类型

  • 输入流:从源读取数据(文件、网络、内存)

  • 输出流:向目标写入数据

  • 双向流:既可读又可写

  • 数据流:处理原始字节或字符

  • 对象流:处理序列化对象

C# 中的流实现

以下是一个简单的自定义流实现:

函数式编程中的流(惰性序列)

流的设计体现了"按需计算"的原则,使得我们能够高效地处理大量数据,同时保持内存使用可控。

6 你在这一周学到了什么?

这是一个个人反思性问题,面试官希望通过这个问题了解候选人的学习习惯和成长意识。以下是一个示例回答框架:

回答框架

1. 技术学习

  • 学习了一个新的框架/库/工具

  • 深入理解了某个技术概念

  • 解决了一个复杂的技术问题

2. 软技能提升

  • 改进了沟通方式

  • 学会了更好的时间管理

  • 提高了团队协作能力

3. 行业洞察

  • 了解了新的行业趋势

  • 学习了最佳实践

  • 从技术文章/会议中获得的启发

示例回答

"这周我学到了几件事情:

在技术方面,我深入研究了 .NET 中的 Source Generator 功能。以前我只是听说过这个概念,但这周我花时间实际动手实现了一个简单的代码生成器。我发现它可以在编译时生成代码,大大减少运行时的反射开销,这对我们正在优化的性能敏感模块很有帮助。

在工作方法上,我学会了更有效地进行代码审查。我注意到以前我的评论往往过于关注细节,这周我开始尝试先关注设计层面的问题,再逐步深入到实现细节。这样的反馈对同事更有帮助,也让代码审查的效率提高了。

另外,我读了一篇关于系统设计中'反脆弱性'的文章,让我对如何构建更健壮的分布式系统有了新的思考。"

回答要点

  1. 具体:给出具体的例子,避免空泛的描述

  2. 反思:展示你是如何思考和内化学到的知识

  3. 应用:说明如何将学到的东西应用到实际工作中

  4. 持续性:表现出持续学习的习惯和意愿

7 所有的设计中都会有美学元素(aesthetic element)的存在。问题是,你认为美学元素是你的朋友还是敌人?

我认为美学元素是软件开发者的朋友,但需要明智地运用。

美学作为朋友

1. 代码美学促进可维护性

优雅的代码通常更容易理解和维护。代码的"美"往往体现在:

  • 清晰的结构和逻辑流

  • 一致的风格和命名

  • 简洁而不失表达力

  • 适当的抽象层次

这些美学特质与实用价值高度一致。

2. 设计美学引导良好架构

好的架构往往具有一种内在的"美感":

  • 对称性和平衡

  • 组件间的和谐关系

  • 概念上的统一性

当设计"感觉不对"时,通常意味着存在潜在问题。

3. 用户界面美学影响用户体验

美观的界面:

  • 增加用户信任

  • 提升用户满意度

  • 让产品更易用

美学可能成为敌人的情况

1. 过度追求美学导致过度设计

2. 形式超越功能

  • 花费过多时间在代码格式化上

  • 为了"优雅"而牺牲性能

  • 追求完美而延迟交付

3. 主观性导致争论

美学往往是主观的,团队成员可能对"美"有不同理解,导致无谓的讨论。

平衡之道

  1. 实用优先:首先确保代码工作正确、性能合理

  2. 渐进美化:在功能完成后再优化代码美学

  3. 团队共识:建立共同的代码规范,减少主观争论

  4. 投资回报:评估美学改进带来的实际价值

"完美是优秀的敌人。" — Voltaire

美学应该服务于目标,而不是成为目标本身。当美学与实用性一致时,它是宝贵的盟友;当它成为交付的障碍时,就需要及时调整优先级。

8 列出你最近读的五本书

这是一个个人化的问题,面试官希望了解候选人的阅读习惯和兴趣领域。以下是一些适合后端工程师的推荐书籍,可以作为参考:

技术类书籍

  1. 《设计模式:可复用面向对象软件的基础》(GoF)

    • 经典的设计模式书籍

    • 提供了23种设计模式的详细解析

    • 帮助理解如何构建灵活、可维护的代码

  2. 《Clean Code》(整洁代码)- Robert C. Martin

    • 代码质量的圣经

    • 提供了编写可读、可维护代码的实用指南

    • 包含大量实际代码示例和重构技巧

  3. 《Designing Data-Intensive Applications》(设计数据密集型应用)- Martin Kleppmann

    • 深入讲解分布式系统的核心概念

    • 覆盖数据存储、复制、分区等关键主题

    • 是后端工程师必读的系统设计书籍

  4. 《The Pragmatic Programmer》(程序员修炼之道)- Andrew Hunt & David Thomas

    • 关于如何成为更好的程序员的实用指南

    • 涵盖从职业发展到技术实践的多个方面

    • 包含许多可立即应用的建议

  5. 《Domain-Driven Design》(领域驱动设计)- Eric Evans

    • 如何将复杂业务逻辑映射到软件设计

    • 介绍了限界上下文、聚合等核心概念

    • 对构建复杂企业应用非常有价值

回答技巧

  1. 准备好讨论:面试官可能会追问书中的具体内容

  2. 展示思考:分享书籍对你工作的影响

  3. 多元化:包括技术书籍和非技术书籍(如管理、沟通类)

  4. 真实性:只说你真正读过的书

9 如何让大公司从瀑布式开发模式到持续交付的模式改变?

这是一个复杂的组织变革问题,需要从技术、流程和文化三个维度同时推进。

1. 建立愿景和获得支持

高层支持

  • 向管理层展示持续交付的业务价值(更快交付、更低风险、更高质量)

  • 用数据说话:行业案例研究、竞争对手分析

  • 识别并争取关键决策者的支持

明确目标

  • 设定可衡量的目标(如:部署频率、交付周期、故障恢复时间)

  • 建立阶段性里程碑

2. 从试点项目开始

选择合适的试点

  • 选择中等复杂度的项目

  • 确保团队有足够的自主权

  • 选择愿意尝试新方法的团队

展示成功

  • 记录改进指标

  • 分享成功经验

  • 建立内部案例研究

3. 技术基础设施建设

自动化流水线

关键技术实践

  • 版本控制(所有代码、配置、基础设施即代码)

  • 自动化测试(单元测试、集成测试、端到端测试)

  • 持续集成(频繁合并代码、快速反馈)

  • 自动化部署(一键部署、环境一致性)

  • 监控和可观测性(日志、指标、追踪)

4. 流程变革

减小批次大小

  • 将大功能分解为小的、可独立交付的增量

  • 每个增量都可测试、可部署

建立反馈循环

  • 短迭代周期(1-2周)

  • 频繁的演示和回顾

  • 快速的缺陷修复流程

质量内建

  • 测试左移(尽早测试)

  • 代码审查

  • 自动化质量门禁

5. 文化转变

克服恐惧

  • 频繁发布小变更比偶尔发布大变更更安全

  • 建立心理安全感,允许从失败中学习

团队结构

  • 跨职能团队(开发、测试、运维一体化)

  • 减少交接,增加协作

  • DevOps文化的推广

持续改进

  • 定期回顾会议

  • 鼓励实验和创新

  • 庆祝成功,学习失败

6. 渐进式推广

7. 度量和反馈

关键指标(DORA指标)

  • 部署频率

  • 变更前置时间

  • 变更失败率

  • 平均恢复时间

通过持续跟踪这些指标,展示改进效果,并识别需要关注的领域。

总结

从瀑布到持续交付的转变不是一蹴而就的,通常需要数年时间。关键是:

  1. 从小处开始,逐步扩展

  2. 同时推进技术和文化变革

  3. 用数据证明价值

  4. 保持耐心,持续改进

10 什么时候你觉得重复发明轮子是有意义的?

"不要重复发明轮子"是软件开发中的常见建议,但在某些情况下,重新发明轮子不仅合理,而且必要。

有意义的场景

1. 学习和教育目的

通过重新实现已有的东西来深入理解其原理:

  • 自己实现一个简单的数据库来理解B+树和事务

  • 实现一个简易的Web框架来理解HTTP协议和路由

  • 构建一个简单的编译器来理解词法分析和语法分析

这种"轮子"的价值不在于替代现有工具,而在于学习过程本身。

2. 特殊性能要求

当通用解决方案无法满足特定性能需求时:

  • 游戏引擎中的自定义内存分配器

  • 高频交易系统中的专用数据结构

  • 对延迟极度敏感的场景

3. 减少依赖

  • 避免引入庞大的库只为使用其中一小部分功能

  • 减少安全风险(第三方依赖的漏洞)

  • 降低维护成本(依赖升级、兼容性问题)

4. 现有方案不完全适用

  • 业务需求独特,现有库无法满足

  • 需要与特定技术栈深度整合

  • 许可证限制(如GPL对商业项目的限制)

5. 核心竞争力

当某个能力是产品的核心差异化因素时:

  • Google的搜索算法

  • Netflix的推荐系统

  • 专有的加密或安全模块

不应该重新发明轮子的情况

  • 现有解决方案已经很成熟且广泛使用

  • 团队没有足够的专业知识来维护自研方案

  • 时间和资源有限

  • 安全敏感领域(如加密算法)除非有专业团队

决策框架

考虑因素
使用现有方案
自己实现

时间压力

紧迫

充裕

团队能力

有限

专业

维护成本

可接受

可承担

定制需求

核心业务

11 我们来谈谈"重复造轮子","非我发明症", "吃自己做出来的狗粮"的这些做法吧。

这三个概念都与软件开发中的技术决策和实践相关,让我们逐一分析。

重复造轮子(Reinventing the Wheel)

定义:重新创建已经存在且运行良好的东西。

利弊分析

优点
缺点

深入理解原理

浪费时间和资源

完全控制代码

可能引入bug

针对特定需求优化

维护负担

减少外部依赖

错过成熟方案的优势

最佳实践

  • 评估现有方案是否真的不满足需求

  • 如果决定自己做,明确范围,不要过度设计

  • 记录决策原因,便于未来评估

非我发明症(Not Invented Here Syndrome, NIH)

定义:因为不是自己团队开发的,就拒绝使用外部解决方案的倾向。

问题所在

  • 基于情感而非理性的决策

  • 浪费资源重建已有功能

  • 错失利用成熟生态系统的机会

  • 增加项目风险和维护成本

识别信号

  • "我们可以自己写一个更好的"

  • "那个库太复杂了,我们不需要那么多功能"

  • "我们不信任外部代码"

应对策略

  1. 建立客观的技术评估流程

  2. 记录使用/不使用外部库的决策原因

  3. 鼓励开放心态,学习外部优秀实践

  4. 关注核心业务,非核心功能倾向使用成熟方案

吃自己的狗粮(Eating Your Own Dog Food / Dogfooding)

定义:在内部使用自己开发的产品或服务。

好处

  1. 发现问题:在用户之前发现bug和可用性问题

  2. 理解用户:开发者成为用户,更能理解用户需求

  3. 建立信任:向客户展示对自己产品的信心

  4. 快速反馈:缩短从问题发现到修复的周期

实践例子

  • 微软员工使用Windows和Office

  • Google员工使用Gmail和Google Docs

  • 开发团队使用自己开发的项目管理工具

注意事项

  • 内部用户可能不代表所有用户群体

  • 开发者可能容忍普通用户无法接受的问题

  • 需要配合其他用户研究方法

三者的平衡

理性决策原则

  1. 核心能力:核心差异化功能可以自研

  2. 非核心功能:优先使用成熟的外部方案

  3. 验证机制:无论自研还是采用,都通过dogfooding验证

  4. 数据驱动:用实际效果(性能、稳定性、开发效率)说话

12 在你当前的工作流中,什么事情是你计划下一步需要自动化的?

这是一个反思性问题,用于了解候选人对效率的追求和自动化思维。以下是一些常见的自动化方向和示例回答:

常见的自动化方向

1. 代码质量自动化

  • 自动代码格式化和风格检查

  • 自动化的代码复杂度分析

  • 安全漏洞自动扫描

2. 测试自动化

  • 自动生成测试数据

  • 端到端测试自动化

  • 性能测试的自动化运行和报告

3. 部署和运维自动化

  • 环境配置的自动化(IaC)

  • 自动化的数据库迁移

  • 日志分析和告警自动化

4. 文档自动化

  • API文档自动生成

  • 代码变更日志自动生成

  • 架构图自动更新

5. 日常任务自动化

  • 重复性的数据处理脚本

  • 定期报告的自动生成

  • 开发环境的一键设置

示例回答

"在我当前的工作流中,我计划下一步自动化以下几个方面:

代码审查前的检查:目前我们的代码审查中很多时间花在格式问题和简单的代码规范上。我计划设置一个pre-commit钩子,自动运行格式化工具和静态分析,这样代码审查可以专注于逻辑和设计层面。

测试数据生成:我们经常需要创建测试数据来模拟各种场景。我计划创建一个数据生成工具,可以根据模型自动生成符合业务规则的测试数据。

部署后的健康检查:目前部署后需要手动验证服务是否正常。我计划创建自动化的健康检查脚本,部署后自动运行并在发现问题时告警。

这些自动化可以每天节省团队30-60分钟的时间,更重要的是减少人为错误。"

回答要点

  1. 具体:说明具体要自动化什么

  2. 价值:解释为什么这值得自动化

  3. 可行性:展示你对如何实现有想法

  4. 优先级:说明为什么这是"下一步"

13 为什么编写软件是复杂的?什么让维护软件也变得非常困难?

软件复杂性是计算机科学中一个经典话题。Fred Brooks在《人月神话》中提出了"本质复杂性"和"偶然复杂性"的概念,这是理解软件复杂性的重要框架。

软件编写复杂的原因

1. 本质复杂性(Essential Complexity)

这是问题本身固有的复杂性,无法简化:

  • 业务逻辑复杂:现实世界的规则往往复杂且充满例外

  • 状态管理:系统可能的状态组合呈指数增长

  • 并发处理:多个事件同时发生带来的复杂交互

  • 分布式系统:网络不可靠、节点可能故障

2. 偶然复杂性(Accidental Complexity)

这是由我们选择的工具和方法引入的复杂性:

  • 不合适的技术选型

  • 过度设计和不必要的抽象

  • 历史遗留的技术债务

  • 团队沟通不畅导致的架构不一致

3. 软件的特殊性质

软件维护困难的原因

1. 理解成本高

  • 代码量庞大,需要时间消化

  • 缺乏文档或文档过时

  • 原始作者已离开团队

  • 隐含的业务知识没有被记录

2. 变更风险

  • 修改一处可能影响其他地方

  • 测试覆盖不全面

  • 对系统行为理解不完整

  • "修复一个bug,引入两个新bug"

3. 技术债务累积

随着时间推移,系统积累了各种妥协和临时方案:

4. 环境和依赖变化

  • 操作系统更新

  • 第三方库升级或废弃

  • 安全漏洞修复

  • 硬件和基础设施变化

5. 知识流失

团队成员变动导致对系统的理解逐渐流失。

应对策略

问题
解决方案

本质复杂性

领域驱动设计、模块化、抽象

偶然复杂性

重构、简化、遵循最佳实践

理解成本

文档、代码注释、知识共享

变更风险

自动化测试、持续集成、小步迭代

技术债务

定期清理、债务可视化、预算分配

总结

软件复杂性是不可避免的,但可以被管理。关键是:

  1. 接受本质复杂性,用好的设计来应对

  2. 积极消除偶然复杂性

  3. 持续投资于可维护性

  4. 建立知识共享和文档文化

14 你更喜欢在全新项目(Green Field Project)上工作还是在已有项目(Brown Field Project)基础上工作?为什么?

这是一个开放性问题,没有标准答案。每种项目类型都有其独特的挑战和机会。以下是对两种项目类型的分析:

Green Field Project(全新项目)

优点

  • 自由度高:可以选择最新的技术栈和最佳实践

  • 无历史包袱:不需要处理遗留代码和技术债务

  • 设计自主:可以从头设计架构,避免已知的反模式

  • 学习机会:有机会尝试新技术和方法

  • 高成就感:从零开始构建有满足感

挑战

  • 不确定性高:需求可能不清晰,方向可能改变

  • 缺乏参考:没有现有代码可以借鉴

  • 基础设施工作多:需要建立很多基础组件

  • 容易过度设计:因为没有约束,可能设计过于理想化

Brown Field Project(已有项目)

优点

  • 有用户基础:产品已经被验证,有真实用户

  • 稳定收入:可能已经在产生商业价值

  • 学习机会:可以从现有代码中学习

  • 影响明确:改进可以立即看到效果

  • 业务知识积累:代码反映了积累的业务知识

挑战

  • 遗留代码:需要理解和维护历史代码

  • 技术债务:可能有大量需要偿还的技术债

  • 约束多:技术选择受限于现有架构

  • 变更风险:修改可能影响现有功能

我的观点

作为一个平衡的回答,可以这样说:

"两种项目类型我都喜欢,因为它们提供了不同的成长机会。

Green Field项目让我能够应用最新学到的知识,从架构层面思考问题。我享受那种从白板上的想法到可运行系统的创造过程。

Brown Field项目则培养了我不同的能力:理解大型代码库、在约束中工作、平衡理想与现实。实际上,在现有系统中进行有意义的改进往往比从头开始更具挑战性。

如果必须选择,我会说我略微偏向Brown Field项目。原因是:

  1. 这反映了真实世界中大多数工作的性质

  2. 改进一个运行中的系统需要更深入的理解和更谨慎的判断

  3. 成功的改进能够立即为用户和业务带来价值

无论是哪种项目,关键是能够学习、成长并创造价值。"

回答技巧

  1. 展示成熟度:表明你理解两种项目的价值

  2. 给出理由:解释你偏好的原因

  3. 保持灵活:表明你能够适应不同类型的项目

  4. 联系经验:如果可能,用具体经验说明

15 当你在浏览器中输入google.com并且按回车后,发生了什么?

按下"g"键

接下来的内容介绍了物理键盘和系统中断的工作原理,但是有一部分内容却没有涉及。当你按下“g”键,浏览器接收到这个消息之后,会触发自动完成机制。浏览器根据自己的算法,以及你是否处于隐私浏览模式,会在浏览器的地址框下方给出输入建议。大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。你打算输入 "google.com",因此给出的建议并不匹配。但是输入过程中仍然有大量的代码在后台运行,你的每一次按键都会使得给出的建议更加准确。甚至有可能在你输入之前,浏览器就将 "google.com" 建议给你。

回车键按下

为了从零开始,我们选择键盘上的回车键被按到最低处作为起点。在这个时刻,一个专用于回车键的电流回路被直接地或者通过电容器间接地闭合了,使得少量的电流进入了键盘的逻辑电路系统。这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除(debounce),并将其转化为键盘码值。在这里,回车的码值是13。键盘控制器在得到码值之后,将其编码,用于之后的传输。现在这个传输过程几乎都是通过通用串行总线(USB)或者蓝牙(Bluetooth)来进行的,以前是通过PS/2或者ADB连接进行。

USB键盘:

  • 键盘的USB元件通过计算机上的USB接口与USB控制器相连接,USB接口中的第一号针为它提供了5V的电压

  • 键码值存储在键盘内部电路一个叫做"endpoint"的寄存器内

  • USB控制器大概每隔10ms便查询一次"endpoint"以得到存储的键码值数据,这个最短时间间隔由键盘提供

  • 键值码值通过USB串行接口引擎被转换成一个或者多个遵循低层USB协议的USB数据包

  • 这些数据包通过D+针或者D-针(中间的两个针),以最高1.5Mb/s的速度从键盘传输至计算机。速度限制是因为人机交互设备总是被声明成"低速设备"(USB 2.0 compliance)

  • 这个串行信号在计算机的USB控制器处被解码,然后被人机交互设备通用键盘驱动进行进一步解释。之后按键的码值被传输到操作系统的硬件抽象层

虚拟键盘(触屏设备):

  • 在现代电容屏上,当用户把手指放在屏幕上时,一小部分电流从传导层的静电域经过手指传导,形成了一个回路,使得屏幕上触控的那一点电压下降,屏幕控制器产生一个中断,报告这次“点击”的坐标

  • 然后移动操作系统通知当前活跃的应用,有一个点击事件发生在它的某个GUI部件上了,现在这个部件是虚拟键盘的按钮

  • 虚拟键盘引发一个软中断,返回给OS一个“按键按下”消息

  • 这个消息又返回来向当前活跃的应用通知一个“按键按下”事件

产生中断[非USB键盘]

键盘在它的中断请求线(IRQ)上发送信号,信号会被中断控制器映射到一个中断向量,实际上就是一个整型数 。CPU使用中断描述符表(IDT)把中断向量映射到对应函数,这些函数被称为中断处理器,它们由操作系统内核提供。当一个中断到达时,CPU根据IDT和中断向量索引到对应的中断处理器,然后操作系统内核出场了。

(Windows)一个 WM_KEYDOWN 消息被发往应用程序

HID把键盘按下的事件传送给 KBDHID.sys 驱动,把HID的信号转换成一个扫描码(Scancode),这里回车的扫描码是 VK_RETURN(0x0d)KBDHID.sys 驱动和 KBDCLASS.sys (键盘类驱动,keyboard class driver)进行交互,这个驱动负责安全地处理所有键盘和小键盘的输入事件。之后它又去调用 Win32K.sys ,在这之前有可能把消息传递给安装的第三方键盘过滤器。这些都是发生在内核模式。

Win32K.sys 通过 GetForegroundWindow() API函数找到当前哪个窗口是活跃的。这个API函数提供了当前浏览器的地址栏的句柄。Windows系统的"message pump"机制调用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam) 函数, lParam 是一个用来指示这个按键的更多信息的掩码,这些信息包括按键重复次数(这里是0),实际扫描码(可能依赖于OEM厂商,不过通常不会是 VK_RETURN ),功能键(alt, shift, ctrl)是否被按下(在这里没有),以及一些其他状态。

Windows的 SendMessage API直接将消息添加到特定窗口句柄 hWnd 的消息队列中,之后赋给 hWnd 的主要消息处理函数 WindowProc 将会被调用,用于处理队列中的消息。

当前活跃的句柄 hWnd 实际上是一个edit control控件,这种情况下,WindowProc 有一个用于处理 WM_KEYDOWN 消息的处理器,这段代码会查看 SendMessage 传入的第三个参数 wParam ,因为这个参数是 VK_RETURN ,于是它知道用户按下了回车键。

(Mac OS X)一个 KeyDown NSEvent被发往应用程序

中断信号引发了I/O Kit Kext键盘驱动的中断处理事件,驱动把信号翻译成键码值,然后传给OS X的 WindowServer 进程。然后, WindowServer 将这个事件通过Mach端口分发给合适的(活跃的,或者正在监听的)应用程序,这个信号会被放到应用程序的消息队列里。队列中的消息可以被拥有足够高权限的线程使用 mach_ipc_dispatch 函数读取到。这个过程通常是由 NSApplication 主事件循环产生并且处理的,通过 NSEventTypeKeyDownNSEvent

(GNU/Linux)Xorg 服务器监听键码值

当使用图形化的 X Server 时,X Server 会按照特定的规则把键码值再一次映射,映射成扫描码。当这个映射过程完成之后, X Server 把这个按键字符发送给窗口管理器(DWM,metacity, i3等等),窗口管理器再把字符发送给当前窗口。当前窗口使用有关图形API把文字打印在输入框内。

解析URL

  • 浏览器通过 URL 能够知道下面的信息:

    • Protocol "http" 使用HTTP协议

    • Resource "/" 请求的资源是主页(index)

输入的是 URL 还是搜索的关键字

当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。

转换非 ASCII 的 Unicode 字符

  • 浏览器检查输入是否含有不是 a-zA-Z0-9- 或者 . 的字符

  • 这里主机名是 google.com ,所以没有非ASCII的字符;如果有的话,浏览器会对主机名部分使用 Punycode_ 编码

检查 HSTS 列表

  • 浏览器检查自带的“预加载 HSTS(HTTP严格传输安全)”列表,这个列表里包含了那些请求浏览器只使用HTTPS进行连接的网站

  • 如果网站在这个列表里,浏览器会使用 HTTPS 而不是 HTTP 协议,否则,最初的请求会使用HTTP协议发送

  • 注意,一个网站哪怕不在 HSTS 列表里,也可以要求浏览器对自己使用 HSTS 政策进行访问。浏览器向网站发出第一个 HTTP 请求之后,网站会返回浏览器一个响应,请求浏览器只使用 HTTPS 发送请求。然而,就是这第一个 HTTP 请求,却可能会使用户受到 downgrade attack_ 的威胁,这也是为什么现代浏览器都预置了 HSTS 列表。

DNS 查询

  • 浏览器检查域名是否在缓存当中(要查看 Chrome 当中的缓存, 打开 chrome://net-internals/#dns <chrome://net-internals/#dns>_)。

  • 如果缓存中没有,就去调用 gethostbyname 库函数(操作系统不同函数也不同)进行查询。

  • gethostbyname 函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系统有所不同_

  • 如果 gethostbyname 没有这个域名的缓存记录,也没有在 hosts 里找到,它将会向 DNS 服务器发送一条 DNS 查询请求。DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。

  • 查询本地 DNS 服务器

  • 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP查询

  • 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询

ARP 过程

要想发送 ARP(地址解析协议)广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。

  • 首先查询 ARP 缓存,如果缓存命中,我们返回结果:目标 IP = MAC

如果缓存没有命中:

  • 查看路由表,看看目标 IP 地址是不是在本地路由表中的某个子网内。是的话,使用跟那个子网相连的接口,否则使用与默认网关相连的接口。

  • 查询选择的网络接口的 MAC 地址

  • 我们发送一个二层( OSI 模型_ 中的数据链路层)ARP 请求:

ARP Request::

根据连接主机和路由器的硬件类型不同,可以分为以下几种情况:

直连:

  • 如果我们和路由器是直接连接的,路由器会返回一个 ARP Reply (见下面)。

集线器:

  • 如果我们连接到一个集线器,集线器会把 ARP 请求向所有其它端口广播,如果路由器也“连接”在其中,它会返回一个 ARP Reply

交换机:

  • 如果我们连接到了一个交换机,交换机会检查本地 CAM/MAC 表,看看哪个端口有我们要找的那个 MAC 地址,如果没有找到,交换机会向所有其它端口广播这个 ARP 请求。

  • 如果交换机的 MAC/CAM 表中有对应的条目,交换机会向有我们想要查询的 MAC 地址的那个端口发送 ARP 请求

  • 如果路由器也“连接”在其中,它会返回一个 ARP Reply

ARP Reply::

现在我们有了 DNS 服务器或者默认网关的 IP 地址,我们可以继续 DNS 请求了:

  • 使用 53 端口向 DNS 服务器发送 UDP 请求包,如果响应包太大,会使用 TCP 协议

  • 如果本地/ISP DNS 服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层 DNS 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回

使用套接字

当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket ,请求一个 TCP流套接字,对应的参数是 AF_INET/AF_INET6SOCK_STREAM

  • 这个请求首先被交给传输层,在传输层请求被封装成 TCP segment。目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取(Linux下是ip_local_port_range)

  • TCP segment 被送往网络层,网络层会在其中再加入一个 IP 头部,里面包含了目标服务器的IP地址以及本机的IP地址,把它封装成一个IP packet。

  • 这个 TCP packet 接下来会进入链路层,链路层会在封包中加入 frame 头部,里面包含了本地内置网卡的MAC地址以及网关(本地路由器)的 MAC 地址。像前面说的一样,如果内核不知道网关的 MAC 地址,它必须进行 ARP 广播来查询其地址。

到了现在,TCP 封包已经准备好了,可以使用下面的方式进行传输:

  • 以太网_

  • WiFi_

  • 蜂窝数据网络_

对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点_ 处理。节点的目标地址和源地址将在后面讨论。

大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点_ 进行处理。

最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域(autonomous system, 缩写 AS)的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP数据报头部 time to live (TTL) 域的值每经过一个路由器就减1,如果封包的TTL变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。

上面的发送和接受过程在 TCP 连接期间会发生很多次:

  • 客户端选择一个初始序列号(ISN),将设置了 SYN 位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号

  • 服务器端接收到 SYN 包,如果它可以建立连接:

    • 服务器端选择它自己的初始序列号

    • 服务器端设置 SYN 位,表明自己选择了一个初始序列号

    • 服务器端把 (客户端ISN + 1) 复制到 ACK 域,并且设置 ACK 位,表明自己接收到了客户端的第一个封包

  • 客户端通过发送下面一个封包来确认这次连接:

    • 自己的序列号+1

    • 接收端 ACK+1

    • 设置 ACK 位

  • 数据通过下面的方式传输:

    • 当一方发送了N个 Bytes 的数据之后,将自己的 SEQ 序列号也增加N

    • 另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个 ACK 包,ACK 的值设置为接收到的数据包的最后一个序列号

  • 关闭连接时:

    • 要关闭连接的一方发送一个 FIN 包

    • 另一方确认这个 FIN 包,并且发送自己的 FIN 包

    • 要关闭的一方使用 ACK 包来确认接收到了 FIN

TLS 握手

  • 客户端发送一个 ClientHello 消息到服务器端,消息中同时包含了它的 Transport Layer Security (TLS) 版本,可用的加密算法和压缩算法。

  • 服务器端向客户端返回一个 ServerHello 消息,消息中包含了服务器端的TLS版本,服务器所选择的加密和压缩算法,以及数字证书认证机构(Certificate Authority,缩写 CA)签发的服务器公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥

  • 客户端根据自己的信任CA列表,验证服务器端的证书是否可信。如果认为可信,客户端会生成一串伪随机数,使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥

  • 服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥

  • 客户端发送一个 Finished 消息给服务器端,使用对称密钥加密这次通讯的一个散列值

  • 服务器端生成自己的 hash 值,然后解密客户端发送来的信息,检查这两个值是否对应。如果对应,就向客户端发送一个 Finished 消息,也使用协商好的对称密钥加密

  • 从现在开始,接下来整个 TLS 会话都使用对称秘钥进行加密,传输应用层(HTTP)内容

HTTP 协议

如果浏览器是 Google 出品的,它不会使用 HTTP 协议来获取页面信息,而是会与服务器端发送请求,商讨使用 SPDY 协议。

如果浏览器使用 HTTP 协议而不支持 SPDY 协议,它会向服务器发送这样的一个请求::

“其他头部”包含了一系列的由冒号分割开的键值对,它们的格式符合HTTP协议标准,它们之间由一个换行符分割开来。(这里我们假设浏览器没有违反HTTP协议标准的bug,同时假设浏览器使用 HTTP/1.1 协议,不然的话头部可能不包含 Host 字段,同时 GET 请求中的版本号会变成 HTTP/1.0 或者 HTTP/0.9 。)

HTTP/1.1 定义了“关闭连接”的选项 "close",发送者使用这个选项指示这次连接在响应结束之后会断开。例如:

不支持持久连接的 HTTP/1.1 应用必须在每条消息中都包含 "close" 选项。

在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了。

服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的::

然后是一个换行,接下来有效载荷(payload),也就是 www.google.com 的HTML内容。服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供之后的请求重用。

如果浏览器发送的HTTP头部包含了足够多的信息(例如包含了 Etag 头部),以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应::

这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。

在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到HTML页面引入的所有资源(图片,CSS,favicon.ico等等)全部都获取完毕,区别只是头部的 GET / HTTP/1.1 会变成 GET /$(相对www.google.com的URL) HTTP/1.1

如果HTML引入了 www.google.com 域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host 头部会变成另外的域名。

HTTP 服务器请求处理

HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。

  • HTTPD 接收请求

  • 服务器把请求拆分为以下几个参数:

    • HTTP 请求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或者 TRACE)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法

    • 域名:google.com

    • 请求路径/页面:/ (我们没有请求google.com下的指定的页面,因此 / 是默认的路径)

  • 服务器验证其上已经配置了 google.com 的虚拟主机

  • 服务器验证 google.com 接受 GET 方法

  • 服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)

  • 如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求

  • 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 "/" ,会访问首页文件(你可以重写这个规则,但是这个是最常用的)。

  • 服务器会使用指定的处理程序分析处理这个文件,假如 Google 使用 PHP,服务器会使用 PHP 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者

浏览器背后的故事

当服务器提供了资源之后(HTML,CSS,JS,图片等),浏览器会执行下面的操作:

  • 解析 —— HTML,CSS,JS

  • 渲染 —— 构建 DOM 树 -> 渲染 -> 布局 -> 绘制

浏览器

浏览器的功能是从服务器上取回你想要的资源,然后展示在浏览器窗口当中。资源通常是 HTML 文件,也可能是 PDF,图片,或者其他类型的内容。资源的位置通过用户提供的 URI(Uniform Resource Identifier) 来确定。

浏览器解释和展示 HTML 文件的方法,在 HTML 和 CSS 的标准中有详细介绍。这些标准由 Web 标准组织 W3C(World Wide Web Consortium) 维护。

不同浏览器的用户界面大都十分接近,有很多共同的 UI 元素:

  • 一个地址栏

  • 后退和前进按钮

  • 书签选项

  • 刷新和停止按钮

  • 主页按钮

浏览器高层架构

组成浏览器的组件有:

  • 用户界面 用户界面包含了地址栏,前进后退按钮,书签菜单等等,除了请求页面之外所有你看到的内容都是用户界面的一部分

  • 浏览器引擎 浏览器引擎负责让 UI 和渲染引擎协调工作

  • 渲染引擎 渲染引擎负责展示请求内容。如果请求的内容是 HTML,渲染引擎会解析 HTML 和 CSS,然后将内容展示在屏幕上

  • 网络组件 网络组件负责网络调用,例如 HTTP 请求等,使用一个平台无关接口,下层是针对不同平台的具体实现

  • UI后端 UI 后端用于绘制基本 UI 组件,例如下拉列表框和窗口。UI 后端暴露一个统一的平台无关的接口,下层使用操作系统的 UI 方法实现

  • Javascript 引擎 Javascript 引擎用于解析和执行 Javascript 代码

  • 数据存储 数据存储组件是一个持久层。浏览器可能需要在本地存储各种各样的数据,例如 Cookie 等。浏览器也需要支持诸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之类的存储机制

HTML 解析

浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成8kB大小的分块传输。

HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树。

解析树是以 DOM 元素以及属性为节点的树。DOM是文档对象模型(Document Object Model)的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如Javascript)的接口。树的根部是"Document"对象。整个 DOM 和 HTML 文档几乎是一对一的关系。

解析算法

HTML不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:

  • 语言本身的“宽容”特性

  • HTML 本身可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来支持它们

  • 解析过程需要反复。对于其他语言来说,源码不会在解析过程中发生变化,但是对于 HTML 来说,动态代码,例如脚本元素中包含的 document.write() 方法会在源码中添加内容,也就是说,解析过程实际上会改变输入的内容

由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。

解析结束之后

浏览器开始加载网页的外部资源(CSS,图像,Javascript 文件等)。

此时浏览器把文档标记为可交互的(interactive),浏览器开始解析处于“推迟(deferred)”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成(complete)”,浏览器会触发“加载(load)”事件。

注意解析 HTML 网页时永远不会出现“无效语法(Invalid Syntax)”错误,浏览器会修复所有错误内容,然后继续解析。

CSS 解析

  • 根据 CSS词法和句法_ 分析CSS文件和 <style> 标签包含的内容以及 style 属性的值

  • 每个CSS文件都被解析成一个样式表对象(StyleSheet object),这个对象里包含了带有选择器的CSS规则,和对应CSS语法的对象

  • CSS解析器可能是自顶向下的,也可能是使用解析器生成器生成的自底向上的解析器

页面渲染

  • 通过遍历DOM节点树创建一个“Frame 树”或“渲染树”,并计算每个节点的各个CSS样式值

  • 通过累加子节点的宽度,该节点的水平内边距(padding)、边框(border)和外边距(margin),自底向上的计算"Frame 树"中每个节点的首选(preferred)宽度

  • 通过自顶向下的给每个节点的子节点分配可行宽度,计算每个节点的实际宽度

  • 通过应用文字折行、累加子节点的高度和此节点的内边距(padding)、边框(border)和外边距(margin),自底向上的计算每个节点的高度

  • 使用上面的计算结果构建每个节点的坐标

  • 当存在元素使用 floated,位置有 absolutelyrelatively 属性的时候,会有更多复杂的计算,详见http://dev.w3.org/csswg/css2/ 和 http://www.w3.org/Style/CSS/current-work

  • 创建layer(层)来表示页面中的哪些部分可以成组的被绘制,而不用被重新栅格化处理。每个帧对象都被分配给一个层

  • 页面上的每个层都被分配了纹理(?)

  • 每个层的帧对象都会被遍历,计算机执行绘图命令绘制各个层,此过程可能由CPU执行栅格化处理,或者直接通过D2D/SkiaGL在GPU上绘制

  • 上面所有步骤都可能利用到最近一次页面渲染时计算出来的各个值,这样可以减少不少计算量

  • 计算出各个层的最终位置,一组命令由 Direct3D/OpenGL发出,GPU命令缓冲区清空,命令传至GPU并异步渲染,帧被送到Window Server。

GPU 渲染

  • 在渲染过程中,图形处理层可能使用通用用途的 CPU,也可能使用图形处理器 GPU

  • 当使用 GPU 用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用 GPU 强大的并行计算能力,用于在渲染过程中进行大量的浮点计算。

后期渲染与用户引发的处理

渲染结束后,浏览器根据某些时间机制运行JavaScript代码(比如Google Doodle动画)或与用户交互(在搜索栏输入关键字获得搜索建议)。类似Flash和Java的插件也会运行,尽管Google主页里没有。这些脚本可以触发网络请求,也可能改变网页的内容和布局,产生又一轮渲染与绘制。

16 当你没有自己的代码在运行,你的操作系统会做一些什么,尽管它看上去的空闲的?

即使在没有用户代码运行的情况下,操作系统(OS)也会执行多种任务,以管理硬件资源和运行系统级的服务。操作系统的这些背后活动确保了系统的稳定性、安全性和响应能力。以下是操作系统在看似空闲时可能执行的一些主要任务:

  1. 进程调度和管理 操作系统会不断地调度和管理后台进程,包括系统服务和守护进程(如日志服务、网络服务、安全更新服务等)。

  2. 内存管理 操作系统会执行内存清理和优化操作,如内存压缩、页交换(swapping)和垃圾收集,以确保有效地利用内存资源。

  3. 设备驱动程序和硬件交互 即使在没有明显用户活动时,操作系统也会与硬件设备通信,包括处理来自硬件设备的中断请求、更新设备状态和监控硬件健康状况。

  4. 文件系统管理 操作系统会执行文件系统的维护任务,如索引更新、碎片整理和错误检查,以保持文件系统的效率和完整性。

  5. 网络通信 管理网络连接和通信,包括维护现有连接、监听新的网络请求和执行网络协议栈的相关活动。

  6. 安全监控和更新 执行安全监控任务,如扫描恶意软件、监控异常活动和应用安全补丁或更新。

  7. 日志记录和监控 收集和记录系统日志,以供故障排除和性能监控使用。

  8. 资源监控和报告 监控系统资源的使用情况(如CPU、内存、磁盘和网络)并生成性能报告。

  9. 能源管理 在系统空闲时执行能源管理策略,如降低处理器速度、关闭未使用的硬件组件和执行睡眠模式,以减少能源消耗。

  10. 系统服务和后台任务 运行和管理系统服务和后台任务,这些可能包括自动备份、系统维护任务和用户定义的计划任务。 尽管操作系统看上去是“空闲”的,但这些后台活动是必要的,它们保持系统的正常运行,优化性能,并确保用户和应用程序的需求能够得到及时和有效的响应。

17 如何向一个5岁的孩子解释什么是Unicode/数据库事务?

假想我们有一个巨大的宝箱,里面装着世界上所有语言的字母和符号,每一个都有它自己的小盒子和一个特别的数字。当你想要写下“你好”用中文,或者“hello”用英文,或者用任何语言写任何东西时,你只需要告诉电脑这些特别的数字,电脑就会从那个巨大的宝箱里找到正确的字母和符号,并且显示在屏幕上。这个巨大的宝箱和所有的特别数字,就是我们称之为Unicode的东西。它帮助电脑理解和显示来自世界上任何地方的语言,就像一种魔法一样!

18 如何维护单体架构(monolithic architecture)?

维护单体架构的应用程序可以挑战性很大,特别是当应用程序变得庞大和复杂时。不过,通过采取一些策略和最佳实践,可以使维护工作变得更加可管理和有效。以下是一些关键的建议:

  1. 模块化设计 封装和模块化:将应用分解为逻辑上独立的模块或组件。每个模块应负责一组相关的功能,并且模块之间的依赖尽可能地松耦合。 界面抽象:通过定义清晰的接口和使用抽象层,隔离模块之间的直接依赖,便于单独修改和替换模块而不影响其他部分。

  2. 代码质量和标准 代码审查:定期进行代码审查,确保代码质量和遵循最佳实践。 代码重构:定期重构代码以提高可读性、可维护性和性能。重构应该是持续的过程,而不是一次性的大规模重写。 自动化测试:建立全面的自动化测试覆盖,包括单元测试、集成测试和端到端测试,确保更改不会破坏现有功能。

  3. 持续集成和部署(CI/CD) 自动化构建和部署:实施持续集成和持续部署流程,自动化构建和测试过程,以及代码的部署,减少人为错误,加快迭代速度。

  4. 文档和知识共享 良好的文档:保持更新的文档,包括架构设计、API文档和开发指南,以帮助新成员快速上手,同时作为知识共享的基础。 团队协作:鼓励团队内部知识共享和跨功能团队合作,以提高团队对应用的整体理解和维护能力。

  5. 性能监控和优化 监控:实施实时监控和警报系统,以便及时发现性能瓶颈和故障。 性能优化:定期分析应用性能,识别并解决性能问题,包括优化数据库查询、缓存策略和资源管理。

  6. 逐步重构 逐步改进:对于大型且复杂的单体应用,考虑逐步将其拆分成更小、更易管理的部分,甚至可能逐步过渡到微服务架构,以提高灵活性和可维护性。 结论 虽然单体架构可能带来一些挑战,但通过采用上述策略和最佳实践,可以大大提高应用的可维护性和扩展性。关键在于持续地评估和改进开发流程、代码质量和系统架构。

19 成为一个专业的开发者意味着什么?

成为一个专业的开发者不仅仅意味着掌握编程语言和技术技能,更关乎于一系列的职业素养、持续学习的态度、以及有效沟通和解决问题的能力。以下是成为专业开发者的一些关键要素:

  • 技术熟练度

  1. 掌握基础:深入理解至少一种编程语言的基础知识,包括数据结构、算法和编程范式。

  2. 技术广度和深度:熟悉多种技术栈和工具,了解其背后的原理和最佳实践。

  3. 持续学习:技术领域不断发展,专业的开发者需要持续学习新技术、新方法和行业趋势。

  • 解决问题的能力

  1. 分析和解决问题:能够有效地识别、分析和解决编程和技术问题。

  2. 创新和创造性:在寻找解决方案时能够进行创新思考,提出新的方法和想法。

  • 质量意识

  1. 代码质量:编写可读、可维护、高效的代码,并采用良好的编码实践(如代码复用、测试驱动开发、代码审查)。

  2. 性能和安全意识:理解和实施性能优化和安全最佳实践。

  • 团队合作与沟通

  1. 有效沟通:能够清晰地表达想法,有效地与团队成员、非技术同事和客户沟通。

  2. 团队合作:能够在团队环境中有效工作,理解团队目标和项目要求,并与他人协作以达成目标。

  • 职业道德和责任感

  1. 诚信和透明度:对自己的工作持有高标准的诚信和透明度,承认错误并从中学习。

  2. 尊重知识产权:理解并尊重软件许可和知识产权。

  3. 用户和客户导向:关注用户和客户的需求,致力于提供高质量和有价值的解决方案。

  • 项目和时间管理

  1. 自我管理:有效地管理时间和任务,能够在截止日期前完成高质量的工作。

  2. 项目管理:了解项目管理的基本原理,能够参与项目规划、执行和回顾。

成为一个专业的开发者是一个持续的过程,涉及到技术技能、软技能和职业态度的不断提升。通过不断学习、实践和反思,开发者可以在职业生涯中不断成长和进步。

20 在你的观点中,软件开发是否是一门艺术,一个手工艺亦或者是工程?

软件开发是一个独特的领域,它融合了艺术、手工艺和工程的元素。这种多维度的性质是由于开发软件不仅仅是关于解决问题,还涉及到创造性思维、精湛的技术和系统化的方法。让我们来分别探讨这三个方面:

艺术 软件开发像艺术一样,因为它需要创造性思维和想象力。开发者在设计和实现新软件或功能时,需要考虑用户体验、美学和直觉导航。代码本身的编写也可以被视为一种表达形式,其中开发者的个性和风格可以体现在代码结构和逻辑构建上。正如艺术家通过颜色和形状表达情感和观点,开发者通过代码和技术解决方案来表达创意和解决问题的方法。

手工艺 软件开发也可以被看作是一种手工艺,因为它需要精细的技巧、专注和经验积累。开发高质量的软件需要对编程语言的深入理解,以及能够有效地使用工具和技术的能力。这里涉及到的不仅仅是编写代码,还包括代码的重构、测试和调试——所有这些都需要手工艺人的耐心和细致。此外,随着时间的推移,通过实践和反思,开发者的技术技能会得到提升,类似于手工艺人不断磨练自己的工艺。

工程 最后,软件开发无疑也是一门工程学,因为它涉及到系统化的设计、开发、测试和维护软件系统的过程。软件工程采用工程原理和实践来解决复杂的问题,确保软件的质量、可维护性和性能。它要求严格的规划、分析和执行,以及对复杂系统中各部分如何协同工作的深入理解。

结论 因此,在我看来,软件开发是艺术、手工艺和工程的交汇点。它既需要创造性思维和个人表达,也需要技术熟练度和精湛的手工技艺,同时还需要工程学的严谨性和系统性方法。这种多方面的性质使得软件开发成为一个既富有挑战性又极具创造性的领域,吸引着不同背景和技能的人才。

21 "喜欢这个的人也喜欢...",如何在一个电子商务商店里实现这种功能?

在电子商务平台中实现“喜欢这个的人也喜欢...”功能,通常需要通过推荐系统来完成。这种类型的推荐系统可以基于不同的技术和算法,下面是一些实现这一功能的常见方法:

  1. 协同过滤(Collaborative Filtering) 协同过滤是一种常用的推荐算法,基于用户之间的相似性和历史行为来预测用户可能喜欢的商品。这种方法可以分为两个主要类型:

用户基协同过滤:找出与目标用户有相似购买或评价历史的其他用户,然后推荐这些相似用户喜欢的商品给目标用户。 物品基协同过滤:直接根据商品的相似性进行推荐。如果一个用户购买(或喜欢)了商品A,系统会找出与A最相似的商品B推荐给该用户。

  1. 基于内容的推荐(Content-Based Recommendation) 这种方法根据商品的特征和用户的偏好来进行推荐。系统分析用户过去喜欢(或购买)的商品的属性,然后找出具有相似属性的其他商品推荐给用户。

  2. 混合推荐系统(Hybrid Recommendation System) 混合推荐系统结合了协同过滤、基于内容的推荐和其他方法,以弥补单一方法的不足。例如,它可以结合用户的行为数据和商品的特征数据来生成推荐。

实现步骤 数据收集:收集用户的行为数据(如浏览、购买、评分和评论数据)和商品的特征数据(如类别、标签和描述)。

数据预处理:清洗数据,处理缺失值,转换非结构化数据为可用于分析的格式。

相似度计算:根据所选推荐方法,计算用户之间或商品之间的相似度。

生成推荐:根据相似度计算结果和可能的额外规则(如多样性和新颖性要求),为用户生成推荐列表。

评估和优化:通过A/B测试或其他评估方法,分析推荐系统的效果,并根据反馈进行优化调整。

技术和工具 实现推荐系统可以使用各种编程语言和框架,Python是最受欢迎的选择之一,因为它拥有丰富的数据分析和机器学习库,如Pandas、NumPy、Scikit-learn和TensorFlow。

注意事项 隐私和合规性:在收集和使用用户数据时,确保遵守相关法律法规,保护用户隐私。 冷启动问题:对于新用户或新商品,由于缺乏足够的数据,推荐系统可能难以生成有效推荐。可以通过引入基于规则的推荐或使用基于内容的推荐来缓解这一问题。 通过这些方法和步骤,电子商务平台可以实现“喜欢这个的人也喜欢...”功能,提高用户满意度和销售转化率。

22 为什么大公司在创新上不如初创公司?

Clayton Christensen 在创新者的窘境arrow-up-right书中,针对创新分为两个种类

  1. 延续性创新

  2. 破坏性创新

对于推动产品新能的改善称为 "延续性创新",而破坏性创新则带来与以往截然不同的价值主张。一般来讲破坏性技术产品性能要低于主流市场的成熟产品,但是拥有一些边缘的客户所看重的特性。

成熟的公司(大公司)在应对各种类型的延续性创新可以锐利进取,积极创新,认真听取客户的意见。但是这些公司往往被客户绊住的脚本,在破坏性技术出现的时候,给了新兴公司(小公司)颠覆它们的可乘之机。

价值网络是一种大公司所在的一种环境,大公司在这个环境下确定客户的需求,采取必要的应对措施,解决问题,征求客户的意见,应对竞争对手,并争取利润最大化。 在价值网络中,每个公司照片那个的竞争的策略,决定了它对新技术的延续性创新和破坏性创新而获得回报。在成熟额企业中,预期回报反过来推动资源流向延续性创新,而不是破坏性创新。

23 最近有哪些东西你是值得骄傲的?

这是一个展示个人成就和自我认知的问题。面试官想了解你的价值观、成就感来源以及你如何评价自己的工作。

回答框架

1. 选择有意义的成就

  • 技术上的突破

  • 项目的成功交付

  • 团队协作的成果

  • 个人成长的里程碑

2. 使用STAR方法

  • Situation(情境):背景是什么

  • Task(任务):你需要做什么

  • Action(行动):你具体做了什么

  • Result(结果):产生了什么影响

示例回答

技术成就示例

"最近让我感到骄傲的是,我成功地将一个核心API的响应时间从平均800ms降低到了50ms。

这个API是我们系统中使用最频繁的接口,每天处理约100万次请求。最初我接手这个任务时,它的性能一直是用户投诉的主要来源。

我做了几件事情:

  1. 使用性能分析工具定位瓶颈

  2. 发现问题在于N+1查询和缺少适当的缓存

  3. 重新设计了数据访问层,引入了批量查询和Redis缓存

  4. 添加了性能监控确保改进持续有效

结果是响应时间降低了94%,用户满意度明显提升,这个改进还被作为团队分享的案例。"

团队协作示例

"我最近感到骄傲的是帮助一位新同事快速上手并独立承担项目。

他刚加入团队时对我们的技术栈不太熟悉。我主动成为他的mentor,创建了一份入门指南,每周与他进行一对一交流,在他遇到困难时提供支持而不是直接给答案。

三个月后,他不仅能独立完成任务,还开始向团队分享他带来的新视角和想法。看到一个人的成长,以及我的帮助在其中起到的作用,这让我感到很有成就感。"

个人成长示例

"最近让我骄傲的是我完成了一个开源贡献。

我一直使用一个流行的.NET库,发现它在特定场景下有性能问题。我没有只是抱怨,而是深入研究源码,找到了问题原因,并提交了一个PR。

这个过程让我学到了很多:如何阅读大型代码库、如何与开源社区沟通、如何写出符合项目规范的代码。最终PR被合并时,我感到非常骄傲——我的代码现在被成千上万的开发者使用。"

回答要点

  1. 真实:选择你真正感到骄傲的事情

  2. 具体:给出具体的细节和数据

  3. 谦逊:展示自豪但不自大

  4. 价值观:通过你选择的成就展示你的价值观

  5. 团队意识:如果适用,提及团队的贡献

Last updated