做刀网站,怎样学做网站,摄影的网站设计特点,百度网页版 入口在开展微服务的过程中#xff0c;了解要考虑哪些因素可能是非常有挑战性的事情。没有可以直接使用的金科玉律。每个过程都是不同的#xff0c;因为每个组织面临的都是不同的环境。在本文中#xff0c;我将从初创公司的角度分享我们学习到的经验和面临的挑战#xff0c;以及… 在开展微服务的过程中了解要考虑哪些因素可能是非常有挑战性的事情。没有可以直接使用的金科玉律。每个过程都是不同的因为每个组织面临的都是不同的环境。在本文中我将从初创公司的角度分享我们学习到的经验和面临的挑战以及我下次引入微服务时会在哪些方面采取不同的做法。核心要点从一个易于抽取的小候选功能开始以便于尽早获得微服务的体验要预先重点关注构建和部署自动化以及监控尽早处理横切性的关注点避免给生产效率带来负面的影响比如为单体应用继续增加功能或者为每个微服务重新实现横切性的关注点将系统的事件驱动功能设计得易于演化考虑采用事件流的方案以减少数据副本的成本并降低添加新微服务的门槛需要注意转换至微服务的过程并不是独立运转的。相反它受到很多环境因素的影响。当心那些阻碍你前进或拖你后腿的环境因素对它们进行相应的调整或者至少要在整个组织中意识到这些问题。在开展微服务的过程中了解要考虑哪些因素可能是非常有挑战性的事情对于小团队来讲更是如此。遗憾的是没有可以直接使用的金科玉律。每个过程都是不同的因为每个组织面临的都是不同的环境。在本文中我将从初创公司的角度分享我们学习到的经验和面临的挑战以及我下次引入微服务时会在哪些方面采取不同的做法。从单体应用到微服务的旅程该如何开始最初从各个方面看我都是从单体应用开始的我们整个团队基于一个相互协作的产品开展工作将其实现为同一个代码库并且基于同一个技术栈。在一段时间内这种方式能够很好地运转。随着时间的推移所有的事情都在演化团队在增长我们为产品不断添加越来越多的特性代码库变得越来越大用户的数量也在不断增长。这听起来非常不错对吧但是……现在要完成一件事情需要非常长的时间会议、讨论和决策都要比以往消耗更长的时间。职责无法清晰地划分明确具体责任需要花费一定的时间比如当出现了 bug 的时候。我们的过程变得更加缓慢生产效率也受到了影响。我们添加的特性越多产品使用起来就越复杂。产品的可用性和用户体验因为不断的特性修改而受损。我们不但没有很好地解决用户的问题反而让他们更加困惑。因为采用单体软件架构我们很难在不影响整个系统的情况下添加新的特性释放新的变更也变得非常复杂即便我们只修改了几行代码也需要重新构建和部署整个产品。这导致部署会具有很高的风险性因此部署的频率也不那么频繁因为新特性的发布非常缓慢。因此对系统进行分离和转换的需求就出现了。在三年前我们改变了产品策略。我们关注可用性和用户体验的提升并将我们的产品 JUST SOCIAL 拆分成了多个独立的应用其中每个应用负责特定的场景。我们不断演化这个理念提供不同的应用来共享文档、实时交流、管理任务、共享可编辑的内容和协作的新闻以及管理 profile。同时我们将整个团队拆分成了多个更小的团队并为每个团队分派了特定的一组协作应用collaboration app从而实现定义了良好的职责划分。我们想要建立自治化的团队能够让他们按照自己的节奏独立地围绕系统不同的组成部分开展工作将跨团队的影响降低到最小。在将我们的产品拆分为多个独立的协作应用并将团队分为多个更小的团队之后接下来顺理成章的步骤就是将自治性和灵活性反映到软件架构中这是通过引入微服务实现的。我们引入微服务的驱动力在于让系统的不同组成部分能够实现自治让他们按照自己独立的节奏开展工作将跨团队的影响降到最低。通过独立地开发、部署和扩展协同应用我们希望能够快速地发布变更。我们的微服务之旅首先是从识别适合采取微服务的候选功能开始的。为了识别合适的候选功能我们必须要考虑如何建模良好服务的核心概念。核心概念遵循服务间松耦合和服务内高内聚的原则。服务内的高内聚通常反映在保持相关行为的一致性方面。在领域驱动设计中相关行为反应为限界上下文Bounded Context。限界上下文是领域模型中的语义边界服务会负责定义良好的一个业务功能限界上下文会对服务进行描述。在我们的场景中我们使用协作应用作为高层级的限界上下文它反映了粗粒度的服务边界。这是一个很好的起点后续我们会将它们拆分为更加细粒度的服务层。我们首先从 JUST DRIVE 的限界上下文开始也就是负责文档管理的协作应用。每个文档都是由作者创建的。作者相关的数据来自 profile而后者又是由 profile 管理的限界上下文来进行管理的这个功能依然位于单体应用中。我们从头构建了一个共存co-existing的服务。它实际上并不完全与当前功能的相同相反我们引入了新的 UI、添加了更多的特性并将数据结构做了重大的变更。新服务的限界上下文包括负责业务逻辑的领域模型、编排用例的和管理事务的应用服务以及输入输出的适配器比如 REST 端点和用于持久化管理的适配器。新服务会独占文档状态也就是说它是唯一能够读取和写入文档的服务。如前文所述每个文档都是由作者创建的作者的数据来源于单体应用所管理的 profile 数据。那么问题就来了新服务和单体应用之间该如何交互呢为了避免每次展现文档的时候都从 profile 服务中获取作者数据我们在新的服务中保留了相关作者数据的一个本地副本。只要不破坏数据的所有权数据冗余是没有问题的在我们这个场景中只要 profile 相关的限界上下文依然独占 profile 状态即可。由于本地副本和原始的数据会随着时间的推移而产生差异所以单体应用需要在 profile 更新的时候通知我们。在 profile 发生变化的时候单体应用会发布一个 ProfileUpdatedEvent 事件新服务需要订阅这个事件。新服务消费该事件并相应地更新本地副本。这种事件驱动的服务集成方式降低了服务之间的耦合因为我们现在不需要跨上下文远程直接查询单体应用了。这种方式增加了自治性新服务能够对本地副本做任何事情而且能够让数据连接join更加高效因为它可以使用本地副本连接作者数据无需通过网络。我们从头构建了一个共存的服务并且为了实现数据复制的目的引入了事件驱动形式的服务交互。我们遇到了什么挑战以及是如何解决的从头开始构建共存的服务通常是一种很好的分解策略当你想要摆脱某些东西的束缚时更是如此比如想要脱离过时的业务逻辑或者现有的技术栈。但是在解耦第一个服务的时候我们一次性做了太多的事情。如前文所述我们不仅从头构建了一个共存的服务还引入了新的 UI、添加了更多的特性还对数据结构做了重大的变更。在开始的时候我们承担了太多的责任所以在很晚的时候才看到结果。但是在开始阶段快速得到结果以获取使用微服务的经验和信心是非常重要的。在下一个备选服务中我们采取了不同的方式。我们关注 chat 应用的高层级限界上下文并遵循自上而下的渐进式分解策略逐步抽取已有的代码。我们首先将 UI 抽取为单独的 Web 应用并在单体应用侧引入了 REST-API这样被抽取出来 Web 应用可以访问该 API。在这一步我们可以独立地开发和部署 Web 应用从而能够对 UI 进行快速迭代。在抽取完 UI 之后我们就可以更进一步解耦业务逻辑。分解业务逻辑会对代码带来重大的变更。根据依赖关系我们可能需要提供一个临时的 REST API 供单体应用使用以解决业务逻辑抽取后所带来的问题。此时我们依然共享相同的数据存储。为了实现非耦合的独立服务我们最终需要切分数据存储以确保新服务能够独占 chat 的状态。在每个 chat 讨论中都会涉及到参与者。chat 参与者的数据来源于单体应用中的 profile 数据。如前面描述的 DRIVE 样例类似我们保存一个 chat 参与者数据的本地副本并订阅 ProfileUpdatedEvent 事件从而让本地副本数据与单体应用中原始数据的保持同步。从此处开始我们就可以继续从单体应用中抽取下一个限界上下文或者将我们的粗粒度服务随后拆分为更细粒度的服务。另外一项挑战是对授权的处理几乎对于每个服务我们都会面临如何授权的问题。我为你描述一个背景授权处理是非常细粒度的一直向下延伸到领域对象级别。每个协作应用都要控制其领域对象的权限比如文档的权限是由该文档所在的父文件夹的授权设置来控制的。另一方面授权不仅仅是细粒度的还依赖于服务之间的交互在某些场景下领域对象的授权还依赖于父领域对象的授权信息而父领域对象的授权信息是位于其他服务中的比如要读取某个内容页相关的文档或者为内容页添加文档的话需要依赖于这个页面的授权设置而这个页面的授权配置位于与文档本身不同的服务中。因为这些复杂的需求解决分布式授权的问题给我们带来了很大的困扰而且我们没有在早期提供解决方案。这样带来的结果完全适得其反。其中一个后果就是我们添加了一个新的服务到单体应用中而单体应用其实早就已经解决过授权的问题了。我们让单体应用变得更大了而不是让它变得更小。另外一个后果就是我们开始在每个服务上都实现授权。起初这种做法看上去是合理的因为我们最初的假设是授权属于领域模型所在的限界上下文但是我们忽略了服务之间的依赖关系。所以我们不断地来回复制数据增加了冲突的风险。长话短说我们最终将授权处理合并到了一个中心化的微服务中。与中心化服务一并出现的是引入分布式单体应用的风险。当修改系统中的某一部分时你必须要同时修改其他的组成部分这是已引入分布式单体应用的强烈信号。以我们的场景为例当引入需要授权的新协作应用时我们需要同时修改中心化的授权服务。我们同时遇到了单体应用和分布式应用的缺点服务是紧耦合的而且服务还需要通过缓慢、不稳定的网络来进行通信。于是我们提供了一个通用的契约这个契约属于授权服务所有的下游服务都必须要遵守该契约。在我们的场景中服务会将授权相关的行为转换成授权服务能够理解的契约授权服务不需要额外的转换。这种转换是在每个下游服务中发生的而不是在中心化的授权服务中发生的。这种通用契约能够确保我们在引入新的服务时不需要同时修改和重新部署中心化的认证服务了。有个先决条件是这个通用的契约是稳定的或者说至少向下兼容否则的话我们会将问题转移给下游服务这会导致它们需要不断进行更新。我们学习到了什么在开始阶段需要特别注意最好从易于提取的小型服务开始以便于快速得到结果并获取使用微服务的早期经验。如果要处理粗粒度的大型服务就我们而言将拆分过程分为增量式的步骤会更加易于管理例如增量式地由上到下进行分解也就是每次只执行一个可管理的步骤。尽早处理横切性的关注点非常重要这样能够避免适得其反的后果比如不断扩大单体应用而不是缩减它或者在每个服务中都重新实现横切性的关注点。在引入中心化的横切服务时需要注意不要引入分布式单体应用。在这种情况下通用且稳定的契约能够帮助我们避免出现分布式单体应用。要设计易于演化的系统事件驱动的服务交互方式是实现服务间高度解耦的关键。事件可以用作通知也可以用于生成数据副本关于事件驱动的状态转移参见上文关于从头构建共存服务的内容我们还可以通过长期保留事件将事件存储作为主要的数据源。当事件单纯用于通知的目的时其他上下文中的额外数据通常会以跨上下文查询的方式直接进行请求比如 REST 请求。我们可能会更喜欢远程查询的简洁性而不愿处理本地维护数据集所带来的开销在数据集会不断增长的情况下更是如此。但是远程查询增加了服务之间的耦合性并且在运行时将服务绑定在了一起。我们可以将对其他上下文的远程查询进行内部化处理这是通过引入相关跨上下文数据的本地副本来实现的。如上面的 JUST DRIVE 样例所述为了避免每次展现文档的时候都从 profile 服务中请求相关的作者数据我们复制了作者数据并在文档微服务中保留了一个本地副本。我们需要保证副本数据和原始数据的同步这意味着当原始数据变化的时候要立即同步我们的本地副本。为了获取已修改数据的通知服务需要订阅包含数据变化的事件并相应地更新本地副本。在本例中事件是用来生成数据副本的这样能够避免远程查询并降低服务之间的耦合性。这种方式也能实现更好的自治性因为服务能够对本地副本执行任何操作。对于事件驱动服务的交互我们在早期就引入了 Apache Kafka这是一个分布式、具有容错性、可扩展的日志提交服务。最初我们使用 Apache Kafka 的主要目的是实现通知和生成数据副本的功能。最近我们引入 Apache Kafka Streams 作为共享的事实源以减少数据复制的开销并实现服务的高可插拔性降低新服务进入的壁垒。流是无界有序且持续更新的结构化数据记录组成的序列。数据记录有一个 key-value 对组成。当你的服务在 Apache Kafka 流上下文中启动时Kafka 主题将会加载到你的流中你可以在服务的范围内处理它。主题通常是一个逻辑分类表明了哪些服务可以发布和订阅。每个流都会缓冲到一个状态存储中这是一个轻量级的基于硬盘的数据。加载的流会在你自己的代码中使用不会在 Kafka 代理中运行它运行在你的微服务进程中。流能够让数据出现在任何需要的地方这会增强性能和自治性。Apache Kafka 提供了一个 Stream API。Stream 可以借助领域特定语言Domain Specific LanguageDSL进行连接、过滤、分组或聚合流中的每条消息都可以使用类似函数的操作进行处理比如映射、转换或窥探等。在实现流处理的时候通常会同时需要流以及进行功能增强的数据库。Kafka 的 Streams API 通过对流和表的核心抽象提供了该功能。在流和表之前其实存在紧密的关联关系也就是所谓的流 - 表二元性stream-table duality。流可以看做表的变更日志流中的每条数据记录都捕获了表中的一次状态变更。表可以视为快照对应于流中每个 key 的最新值。当我们想要展现一条文档及其作者数据时借助 Kafka Streams我们可以这样做文档服务根据 document 主题创建一个 KStream并根据 profile 主题得到的作者相关 profile 数据来完善该文档。在这个增强的过程中文档服务会根据 profile 主题创建 KTable。现在我们可以将流和表进行连接并将它的结果保存为新的状态存储这样就可以在外部进行访问了运行方式类似于内置的 Materialized View。每当 profile 或文档更新的时候它相关的 Materialized View 也会进行更新。将 Apache Kafka Streams 与其他的事件驱动方式进行对比的话它不需要维护本地副本这减少了维护数据副本和保持数据同步的开销。Apache Kafka Streams 会将数据推送到需要的地方并且运行在与服务相同的进程中。它增加了可插拔性你可以插入新的服务并立即使用流不需要搭建额外的数据存储。它能够减少开销增强性能、自治性并降低新服务的进入壁垒。这个转换的过程并不是隔离运行的它会受到各种环境因素的影响团队的规模、结构和技能都会影响到怎样做才是可控的尤其是在开始阶段如果是一个的团队并且 DevOps 经验很欠缺的话将会对转换的速度造成一定的影响。你的转换过程还会受到一个因素的影响那就是你依然要处理遗留的系统。维护它所耗费的时间会相应地减少进行转换的时间。运行时环境也会影响这个过程。你是在内部环境中运行还是作为云原生应用运行你是否能够依赖托管服务比如托管的 API- 网关还是需要自行搭建和维护如果你的策略是在短期内引入新特性的话那么就会面临决策上的纠结那就是将新需求在何处实现如果作为新的独立服务的话会耗费一定的时间如果采取快捷的方式将其添加到单体应用上那就会带来让单体应用越来越大而不能对其进行缩减的风险。注意那些阻碍前进或减缓速度的环境因素并相应地调整它们或者至少在你的组织中引起注意。记住: 每一次过程都是不同的你的过程可能和我们的完全不同。如果下次继续引入微服务的话在哪些方面的做法会有所不同首先我会检查组织的战略是否与微服务的目标相一致那就是最大化产品的敏捷性以及独立快速地发布变更例如如果你的组织关注较长的发布周期并希望将所有内容部署在一起那么微服务可能不是最佳选择因为无法充分利用微服务的优势。如果你决定采用微服务的话每个人都必须投入其中包括管理层。每个人都需要意识到这个过程是非常复杂和耗时的当你还没有多少经验的时候更是如此。与产品相符的、跨功能的、自治的团队可以很好地与微服务架构模式协作但是应该尽早考虑向 DevOps 文化的转变。每个团队都应该为持续的迭代做好准备并且能够开发、发布、运维和监控他们负责的服务。将单体应用拆分成多个独立的服务只是整个过程的一部分而如何运维它们则是另外一回事儿。你拥有的服务越多它们的自动化构建和部署流程就变得越重要。如果我重做一次的话我将从一个易于抽取的小型候选服务开始不仅要关注它的拆分还要关注构建和部署的自动化并预先监控第一个服务它可以作为后续服务的基础。要搭建这个基础环境可能需要从每个组抽取一个人形成一个临时的任务组。每个微服务从一开始就应该有自己的 CI/CD 管道。另一个需要考虑的问题是将每个微服务进行容器化从而能够得到轻量级、封装好的运行时环境它能够在各个阶段中保持一致如果你以后想要在云环境中运行服务的话更需如此。另外还需要尽早考虑监控的问题包括日志聚合。监控不仅包括服务器还包括服务指标如请求延迟、吞吐量和错误率以便于跟踪服务的健康状况和可用性。要形成结构化和标准化的日志输出如时间格式如 ISO8601和时区如 UTC并引入具有 correlation id 和日志聚合的请求上下文这有助于问题的诊断和剖析。很多事情需要预先处理这非常耗时并且需要得到整个组织的关注。微服务是实现最大化产品敏捷性的投资而不在于削减成本。为了保持在市场上的竞争力产品的敏捷性和持续改进是区别于竞争对手的关键因素。微服务可以提升产品的敏捷性并持续改善但是它需要每个人的贡献包括管理者。关于作者Susanne Kaiser 是来自德国汉堡的独立技术咨询师她曾经担任过初创公司的 CTO并将该公司的 SaaS 解决方案从单体架构迁移为微服务架构。她具有计算机科学的背景在软件开发和软件架构方面有超过 15 年的经验经常在国际性的技术会议上演讲。原文地址https://www.infoq.cn/article/31IdBpWgTQZU7e5-uwh1.NET社区新闻深度好文欢迎访问公众号文章汇总 http://www.csharpkit.com