@levinzhang
2018-08-07T23:14:26.000000Z
字数 10702
阅读 479
by
本文提供了一些如何管理微服务中数据的实际样例,突出强调了如何从单体数据库进行迁移。推荐的做法是首先构建单体的架构,在实际需要微服务的扩展性和其他收益的时候,再迁移至微服务的架构。
本文根据QCon旧金山2017上Randy Shoup上的演讲改编,他是Stitch Fix的工程副总裁(VP)。
我是Stitch Fix的工程副总裁Randy Shoup,根据我的背景,我会讲解如何管理微服务中的数据的课程。
Stitch Fix是美国的一家服装零售商,我们使用技术和数据科学帮助顾客查找他们喜欢的服装。在Stitch Fix之前,我担任过“流动的CTO服务提供者(CTO as a service)”,帮助企业讨论技术和他们的处境。
在职业生涯的早期,我曾经在谷歌担任Google App Engine的主管。这是谷歌的平台即服务,类似于Heroku、Cloud Foundry等等。我更早在eBay担任了六年半的首席工程师,在那里我帮助团队建立了数代的搜索基础设施。如果你曾经用过eBay并且找到了你想要的东西,那说明我们的团队做的很棒。如果你没有找到想要的东西的话,那你现在也应该知道该将责任归咎于谁了。
我们首先会简要介绍一下Stitch Fix,因为这涉及到将单体拆分为微服务的经验教训和技术。Stitch Fix与标准的服装零售商截然不同。我们不是在线商店或者要求顾客到店里,如果有位专家专门为你做这些事情,那又是什么样的体验呢?
我们会要求你填写一个非常详细的个人风格问卷,包括60到70个问题,大约需要20到30分钟。我们会询问你的体型、身高、体重、喜欢什么样的风格、你是否想要炫耀自己的手臂、是否希望将臀部藏起来……我们会问一些非常详细和个人化的事情。为什么要这样做呢?在生活中,能够知道如何为你推荐衣服的人必须是了解你的人。我们明确地询问这些问题并利用数据科学来实现我们的目标。我们会为客户投递五件货物,这些货物是由全国范围内的3500名造型师中的某一位所挑选的。你可以选择保留喜欢的货物,付钱给我们,然后将其余的货物免费寄还给我们。
在幕后,人工和机器层面都会发生好多事情。在机器层面,我们每晚都会检查每件库存,参考每个客户,计算出预期购买的可能性。换句话说,也就是针对我们发送给他的衬衣,计算Randy留下它的条件概率。设想一下,有72%的概率Randy会保留这件衬衫,54%的机会留下这条裤子,47%概率会留下这双鞋,对于在座的每个人,百分比会有所不同。我们有一个机器学习模型,将其放到一个整体中计算这些百分比,它是由针对每个客户的一组个性化推荐算法组成的,造型师会使用这些模型的结果。
造型师本质是在为你购物,站在你的角度上选择五件商品,他(或她)查看这些推荐算法并确定将哪些货物放到邮寄给客户的包裹中。
我们还需要人工将它们组合在一起,这是机器目前无法做到的。有时候,人类能够回答“我将要去曼哈顿参加一个晚上举行的婚礼,所以给我推荐一些合适的衣服”。机器不知道如何应对,但是人类知道机器所不知道的事情。
所有的这些都需要大量的数据。Stitch Fix的数据科学家和工程师的比例是一比一,这是非常有意思的,我相信也是独一无二的。我们的团队中有一百多名软件工程师,大约有八十位数据科学家和算法开发人员,他们从事所有数据科学的工作。据我所知,在行业内这是一个独一无二的比例。我不知道还有哪家公司能够做到这种接近一比一的比例。
我们利用这些数据科学家都做些什么呢?事实证明,如果你足够聪明,就会有回报。
我们对要购买的衣服也采用相同的技术。我们针对购买者运行推荐算法,这些算法会判断得出,下一季我们会购买更多的棉布衣服、露肩装或紧身长裤。
我们将数据分析应用到库存管理中:我们要将哪些货物放到哪个仓库中等等。我们使用它来优化物流和选择运输公司,这样的话,就能以最低的成本将货物及时送达您的家中。我们还会做一些标准的事情,比如需求预测。
我们是一家实体企业:我们会真正地购买衣服、将它们放到仓库中并将其寄送给你。这不像eBay、Google以及一些虚拟企业,如果我们对需求的猜测是错误的,如果需求是我们预期的两倍,那这就不是什么值得庆祝的好事了。这将会是一场灾难,因为我们只能为一半的人提供服务。如果我们有成倍的客户,那我们就需要成倍的仓库、造型师、雇员等类似的东西。对于我们来说,掌握正确的信息非常重要。
在这里,我们利用人工的地方都是人工做的最好的,而使用机器的地方也是机器做的最好的。
当你设计这种规模的系统时,我希望你要有一系列的目标。你希望确保开发团队能够继续独立且快速地往前发展,这就是我所称的“特性敏捷(feature velocity)”。我们想要可扩展性,所以随着业务的增长,就会想要基础设施也随之增长。我们想要组件能够根据负载进行扩展,以满足我们对它们的要求。同时,我们还想要这些组件是有弹性的,所以故障要进行隔离,不要让基础设施产生级联。
具有这些需求的高效组织有很多工作需要完成。DevOps手册中包含了来自Gene Kim、Nicole Forsgren和其他人针对高效组织和低效组织差异性的研究。高效组织运转地更快,也更稳定。你不必在速度和稳定性之间做非此即彼的选择,你可以二者兼得。
高效的组织每天会进行多次部署,而不是每月一次,从代码提交至源码控制工具到部署中间的延迟不超过一个小时,而在其他的组织中,这个过程可能会耗费一周。这是速度方面的差别。
在稳定性方面,高效的组织在一个小时之内就能从故障中恢复过来,而在低效的组织中这可能会耗费一天的时间,并且前者的故障率会更低。高效的组织很少会出现部署有问题的版本并不得不进行回滚操作,而更慢的组织可能会有一半的时间在处理这种情况。这是一个巨大的差异。
其实,不仅仅是速度和稳定性,甚至不仅仅是技术指标。在盈利能力、市场占有率以及生产力方面,高效的组织超出业务目标的可能性是低效组织的2.5倍。所以这些不仅对于工程师非常重要,它们也关系到业务人员。
当我担任“流动的CTO服务提供者”时,经常会被问到的一个问题就是“嘿,Randy,你在Google和eBay都工作过,那么告诉我们一下在那里你们是怎么做的吧。”
我会回答说:“我承诺会告诉你这些事情,但是你们得保证不要跟着这样做”。我说这些并不是为了保守Google和eBay的秘密,而是因为Google具有15000人所组成的工程团队,和只有五人围坐在会议室的创业团队相比,他们面临的是一些不同的问题。这是三个数量级级别的差异,对于不同规模的公司,会有不同的解决方案。
尽管如此,我还是很乐意告诉大家,众所周知的这些公司是如何演进至微服务的——它们并不是一开始就是微服务,而是随着时间演化至此的。
eBay正在对其基础设施进行第五次的重写。在1995年的时候,它起初是一个单体的PERL应用,当它的创立者希望尝试Web的时候,他花费了三天的劳动节和周末假期,最终构建出了eBay。
下一代架构是一个单体的C++应用,在糟糕的场景中,单个DLL有340万行代码。它们甚至到达了编译器对类中方法数量的限制,也就是16,000个。我相信很多人都认为他们的应用是单体架构的,但是很少有人能够比这更糟糕。
第三代是使用Java进行重写的,但是我们不能将其称为微服务,它是小应用(mini-application)。他们将应用拆分为200个不同的Java应用。其中一个负责搜索部分,另一个负责购买部分等等。eBay当前的实例可以称为一组多语种(polyglot)的微服务。
Twitter经历了类似的演化,现在大致是其第三代。它起初是一个Rails应用,别名为Monorail。第二代将前端抽取到了JavaScript中,而后端修改成了Scala编写的服务,因为Twitter是该技术的早期采用者,我们现在可以将其称为多语种集合的微服务。
Amazon.com尽管没有明确的分代,但是他们经历了类似的演化。它最初是一个单体的C++和Perl应用,我们在产品页面中依然能够看到它存在的痕迹。在Amazon.com URL中我们有时会看到“obidos”,这就是最初Amazon.com应用的代码名。Obidos是亚马逊河上的一个地方,位于巴西,这也是它得名的原因。
Amazon.com按照面向服务的架构在2000至2005年间重写了所有的内容。服务主要是通过Java和Scala编写的。在这个阶段中,Amazon.com在业务上并不是特别突出。但是Jeff Bezos坚定信念并强迫(或者说强烈鼓励)公司中的每个人以面向服务的架构重构所有内容。现在,我们可以公平地将Amazon.com归类为多语种集合的微服务。
这些故事都遵循了通用的模式。没有人是从微服务开始的。但是,经过一定的扩展(这种扩展可能只有0.1%的人能够遇到),每个人最终都实现了我们所谓的微服务。
没有人开始就是微服务,经过特定的扩展,每个人最终都形成了微服务。
我要说的是,如果你最终没有后悔自己早期的技术决策,那么你可能过度工程了。
想象一下在1995年eBay的竞争者和Amazon.com的竞争者。如果这家公司不去寻找恰当的产品市场、业务模型以及人们肯花钱支付的领域,而是构建了一个五年之后才可能用上的分布式系统,那么我们没有听到过这家公司也是事出有因的。
同样,思考你的业务在什么地方、你在团队中的位置。如果你是小型的创业公司的话,Amazon.com、Google和Netflix的方案并不一定是适合你的方案。
我喜欢在定义微服务的“微”的时候,代指的不是代码的行数,而是接口的范围。
微服务有单一的目的以及简单、定义良好的接口,它是模块化和独立的。值得关注和探究的关键问题在于,正如Amazon.com所发现的那样,有效的微服务具有隔离的持久化。换句话说,微服务不应该与其他的服务共享数据。
对于可靠地执行业务逻辑并保证不可变性的微服务来说,我们不能让人们阅读和写入服务背后的数据。eBay以另外一种方式发现了同样的问题。eBay在2008年组织很多非常聪明的人付出了很大的努力之后,构建出了一个服务层,但是它并不成功。尽管服务构建地非常好,接口非常棒并符合正交的原则(他们花费了很多的时间来进行思考),但是在接口下面是大量的共享数据库,这些数据库对应用是直接可用的。为了完成应用层的工作,人们并不是必须要使用服务层,所以他们干脆就不使用这个服务层。
在Stitch Fix,我们经历了自己的发展历程。我们并没有构建过单体的应用,但是我们所面临的单体问题是我们所构建的单体化数据库。
我们正在拆分单体数据库并从中抽取服务,但是有一些非常棒的东西我们还希望能够保留。
图1展示了我们所面临问题的一个简单视图。我们的应用要比这个数量更多,但是在一张图片上只能放下这么多的东西了。
图1:Stitch Fix的单体、共享数据库
我们实际上有一个包含所有内容的共享数据库,也就是Stitch Fix感兴趣的所有数据。它包括客户、我们所投递的包裹、包裹中盛放的货物、关于库存的元数据(如样式和SKU)、仓库的信息,这大约有175张不同的表。我们还有70到80个不同的应用程序和服务,它们为了完成各自的工作,都要使用相同的数据库。这就是问题所在。对于团队来讲,共享数据库就是一个耦合点,导致他们相互依赖,而无法做到相互独立。这也是单点故障和性能的瓶颈。
我们的计划是从共享数据库中解耦应用和服务。这里需要很多的工作。
图2:拆分共享数据库
图2展示了拆分共享数据库的步骤。图A是起始点,这里进行了省略,如果将所有内容都画进去的话,那么真实的图像将会充满了方框和线,所以假设只有三张表和两个应用程序。在样例中,我们要做的第一件事情就是构建一个服务来代表客户信息(B)。这将是一个微服务,具备定义良好的接口。在创建该服务之前,我们会与该服务的消费者就该接口进行协商。
接下来,我们让应用从服务中读取数据,而不是使用共享数据库从表中进行读取(C)。这里最难的部分就是这些线的移动。这并不是危言耸听,但是简单的一张图真的无法表现其中的困难。在完成这一点之后,调用者不能直接连接数据库,而是要经过服务。然后,我们将这个表从共享数据库中移除,并将其放到一个隔离的私有数据库中,这个数据库只与该微服务关联(D)。这里会涉及很多较困难的工作,这是一种模式。
接下来的任务就是为货物(item)信息执行相同的处理过程。我们创建一个货物服务,应用使用该服务而不再直接使用表(E)。然后,我们提取该表并将其变成微服务的一个私有数据库。接下来,为SKU或样式执行相同的过程,并不断地优化和重复(F)。最后,每个微服务的边界都包含了代表应用程序的方框及其数据库,比如成对出现的client-service和“core client”数据库(F)。
我们已经拆分了单体数据库,每个表都放在了合适的位置上,所以每个微服务都有自己的持久化。但是,单体数据库有很多有价值的功能,我并不想放弃它们。这包括非常便利地跨不同的服务和应用共享数据、能够便利地跨表执行联结以及事务性。我想要就像一个原子单元那样跨多个实体进行操作。这些都是单体数据库的通用功能。
在接下来的迁移中,对于各种数据库特性,我们有的能够保持,有的则无法继续保持,但是对于无法保持的特性,也都有变通的办法。在深入介绍这些内容之前,我需要指出一个架构性的构建块,你可能知道它但是并没有对其产生应有的兴趣,它就是事件(event)。维基百科对事件的定义是状态上的显著变化或者发生某些感兴趣事情的声明。
在传统的三层系统中,用户或客户端使用展现层,应用层代表了无状态的业务逻辑,持久化层则是由关系型数据库作为支撑的。但是,作为架构师,我们缺少一个代表状态的变化的基础构建块,这也就是我所谓的事件。因为事件一般都是异步的,可能我生成的事件并没有人监听,也可能系统中只有一个消费者在监听,还可能有很多的消费者会订阅它。
在架构中,在将事件提升至第一级构造之后,我们将会把它们用到微服务中。
微服务接口都会包括一个前门,对吧?它显然要包括同步的请求和响应。它一般会是HTTP协议,或者是JSON,或者是gRPC,反正诸如此类吧,但是它显然需要包括一个入口点。但是它还包括了一些并不那么明显的内容(我希望我能说服你它们真的存在),那就是服务所产生的所有的事件、服务消费的所有事件以及服务传入和传出数据的所有方式。为了进行分析而读取服务中的大量数据或者为了上传而向服务批量写入数据都是服务接口的一部分。简而言之,我认为服务的接口包括传入和传出数据的所有机制。
在将事件纳入工具集之后,我们就要使用事件作为工具来解决共享数据、联结和事务方面的问题。首先看一下共享数据的问题。在单体数据库中,使用共享数据是非常容易的。我们将应用指向共享的表就万事大吉了。但是,在微服务领域该在什么地方共享数据呢?
我们有多种不同的可选方案,但是我首先会为你介绍一个在讨论该问题时的工具或术语。这个原则或者术语叫做“单系统记录(single system of record)”。如果系统中存在感兴趣的客户、货物或包裹数据的话,那么应该有且仅有一个服务作为该数据的权威系统记录。系统中应该只有一个服务拥有客户数据、拥有货物数据或拥有包裹数据。系统中会有客户、货物等数据的各种表述形式(当然要在Stitch Fix之中),但是系统中的其他副本必须是只读的,是系统记录的非权威缓存。
再深入解释一下:只读和非权威。不要在任何地方修改客户记录并期望它能够在其他系统中得到保持。如果需要修改数据的话,我们需要去系统记录,这是目前唯一能够在毫秒级就告诉我们用户正在做什么的地方。
这就是系统记录的理念,有多种不同的技术可以按照这种方式来使用,实现数据的共享。第一个是最为明显和简单的:从系统记录中以同步方式进行查找。
我们考虑Stitch Fix中的交付服务。在这个服务中,我们要将一件东西寄送到客户的实际物理地址。客户服务拥有客户本身的数据,其中有一项就是客户的地址。交付服务有一种可选方案就是调用客户服务并查找地址。这种方式并没有什么错误,这是最合法的做法。但是,有时候这种做法并不合适。我们可能并不想让所有的功能都与客户服务耦合。交付服务或其他类似的服务可能会频繁访问客户服务,从而影响到性能。
另外一种方案要将异步事件和本地缓存结合起来。客户服务依然掌握客户的表述,但是当客户数据变更时(比如客户地址),客户服务会发送一个更新事件。当地址发生变化的时候,交付服务会监听到该事件并在本地缓存地址信息,然后交付服务就能很愉快地投递这个包裹了。
交付服务中的缓存还有其他很好的属性。如果客户服务没有保留地址变化的历史的话,我们可以在交付服务中记住它。在一定情况下这是有可能发生的:客户在发起订单和我们交付之间修改了地址信息。我们希望确保能够将订单发送至正确的地方。
在单体数据库中,进行表联结是非常容易的。我们只需要在SQL语句的FROM子句中添加另一个表的名称就万事大吉了。在所有的表都位于一个大的、单体数据库中时,这种方式是很好的,但是如果A和B是独立的服务,那么在SQL语句中就无法这样使用了。一旦我们跨微服务切分数据,从概念上来讲,联结就更难实现了。
在架构上我们始终都会有解决方案,有不止一种方式来处理联结。第一种方式是在客户端进行联结。在A和B实现联结时,将所有感兴趣的内容都取出来。在这个样例中,我们假设要生成一个订单的历史。当客户进入Stitch Fix想要查看我们所发送给他的包裹历史时,我们可能会按照这种方式来为客户提供页面。我们首先可能会让订单历史页面调用客户服务获取当前版本的客户信息,包括他的姓名、地址以及我们为他发送了多少商品。然后,需要访问订单服务获取客户所有订单的详细信息。这里的过程就是从客户服务中获取一条客户信息,然后通过订单服务查询匹配该客户的所有订单。
在无法通过一个服务获取所有数据的Web页面中,这都是一个基本的模式。同样,这是该问题的一个完全合法的解决方案。在Stitch Fix,我们一直这样做,我相信在你的应用中也全是这样的用法。
但是,我们假设这种方式无法使用了,至于原因可能是性能、稳定性或者我们对订单服务执行了太多的查询。
接下来就是第二种方案,我们创建一个服务,按照数据库的术语,我喜欢将其称为“物化视图(materializing the view)”。假设我们要编写一个货物反馈服务(item-feedback)。在Stitch Fix,我们将包裹发送出去,人们会将我们发送给他的商品留下一部分,然后将剩余的退还回来。我们想要知道原因,同时我们还会记住人们留下了什么商品、退还了什么商品。这些内容我们希望通过货物反馈服务来进行记录。我们可能会有1000或10000件特定型号的衬衫,在每次发送给客户时,我们都想记住所有客户对它的反馈。这样按照乘法计算下来,就能会有成千上万的记录信息。
为了实现该功能,我们需要有一个货物服务,它代表了衬衫的元数据。货物反馈服务要监听来自货物服务的事件,比如新的货物上架、已有货物下架以及感兴趣的元数据变更。它还需要监听来自订单服务的事件。每个订单的反馈都要生成一个事件,也就是说如果我们在包裹中放置了五件商品,那么可能就会有五个事件。订单反馈服务监听这些事件并物化联结。换句话说,它会记住每个货物所得到的反馈,并将其放到一个缓存的位置中。换个更有意思的说法就是,它在本地的存储中维护了货物和订单的非规范化联结。
很多常见的系统其实一直都这样做,我们甚至都没有想到他们会采用这样的做法。比如,所有企业级(也就是需要花钱的)的数据库系统都有一个物化视图(Materialized view)的概念。Oracle、SQL Server以及很多企业级数据库都有物化视图的概念。
大多数的NoSQL系统都是按照这种方式运行的。所有受到Dynamo启发创建的数据存储,比如DynamoDB(来自Amazon)、Cassandra、React或Voldemort,都来源于相同的NoSQL传统行为,也就是强迫我们提前就把事情做好。关系型数据库对简单的写入进行了优化,也就是写入到单个记录或单个表中。在读取的时候,我们再将其组合在一起。大多数的NoSQL则恰恰相反。我们所要存储的表就是想要进行查询时所使用的表。我们不会在写入时将数据写到多个子表中,而是向我们后续想要读取的存储查询(stored queries)中写入五次。每个NoSQL系统都强制我们预先进行这种类型的物化联结。
我们所使用的搜索引擎几乎都是将一个特定的实体与另外一个特定的实体按照某种特定的形式联结起来。所有的分析系统都会联结大量的数据,因为这就是分析系统的意义所在。
通过讲解,我希望你能够对这项技术感觉更加熟悉。
关系型数据库非常棒的一点就是其事务的概念。在关系型数据库中,事务包含了ACID属性:也就是原子性、一致性、隔离性和持久性。在单体数据库中,我们可以实现这一点。在我们的系统中,这是数据库所带来的好处之一,跨实体实现事务是非常容易的。在SQL语句中,我们开始事务、执行插入和更新,然后提交,所有的操作要么全部完成,要么全部取消。
将数据进行跨服务分割会让事务变得非常困难。我甚至想把“困难”改成“不可能”。我是怎么知道不可能的呢?在数据库社区有一些技术来实现分布式事务,比如两阶段提交,但实际上几乎没有人真的这样做。作为这一现实情况的论据,那就是整个世界上没有云服务会实现分布式事务。为什么呢?那是因为它是可伸缩性杀手。
所以,我们不再具备事务功能,但是依然有其他的做法。比如有个想要更新A、B和C的事务,我们将它们放到一个saga中,这些操作要么作为一个整体全部执行,要么全部不执行。要创建一个saga,我们需要将事务建模为一个由单个原子事件组成的状态机。图3有助于更清晰地理解这一点。我们按照一个工作流的方式重新实现更新A、更新B和更新C。更新A会生成一个事件,该事件会被服务B所消费。服务B完成自己的工作,并生成一个供服务C消费的事件。最后,状态机最终是一个终止状态,A、B和C都完成了更新。
图3:工作流和sagas
现在,假设某个地方出错了,我们需要按照相反的顺序应用补偿操作。我们撤销在服务C中所做的工作,它会产生一个或多个事件,然后撤销在服务B中所做的工作,这也会产生一个或多个事件,最后我们撤销在服务A中所做的工作。这就是saga的核心概念,在它的背后有很多的细节。如果你想要了解saga的更多知识的话,我强烈推荐Chris Richardson的QCon演讲 在微服务中借助Sagas实现数据一致性。
如物化视图类似,我们日常使用的很多系统都是按照这种方式运行的。考虑一个支付处理系统,如果你想要通过信用卡为我付款,我希望看到钱从你的账号转走,然后奇迹般地出现在我的钱包中,并且这一切都是在一个工作单元中完成的。但事实并非完全如此,在幕后有很多的事情要涉及到支付处理,并且要与很多银行进行对话,这些都像是金融魔术。
在我们讲解事务的经典例子中,我们要从Justin的账户中转走一些钱,然后将其添加到Randy的账户中。世界上的金融系统没有一家是这样运作的,相反,每个金融系统都将其实现为一个工作流。首先,钱会从Justin的账户取出,并且要在银行存放几天。它在银行的存放期要比我想象得还长,但是它最终会出现在我的账户上。
举另外一个费用审批的例子,也许每个人在大会结束后都要进行费用报销。这并不是立即就能完成的。你要将费用提交给你的主管,他批准之后,再将其报告给他的老板,老板再审批通过……层层审批。报销还要经历一个付款处理流程,最终钱会到达你的钱包和账户上。你希望这个过程是一个整体的单元,但是它实际上是以工作流的形式来完成的。任意多步的工作流都与之类似。
考虑最后一个样例,如果你以编写代码为生,假设你在IDE中点击回车键,你的代码就会部署到生产环境中,想象一下这将会发生什么呢?其实没有人会这样做。这并不是一个原子性的事务,它也不应如此。在持续交付管道中,当我要提交的时候,要做很多的事情,最终我们希望能够部署到生产环境上。这是高效组织的做法。但是,这些并不是一个原子的过程。同样,它是一个状态机:这件事完成,执行下一件,然后再下一件,如果这个过程中有错误的话,我们就会进行回滚。这听起来非常熟悉。我们每天使用的系统都是这种行为方式,这意味着在我们构建的服务中采取这种技术是没有什么问题的。
概况来讲,我们探讨了如何将事件作为架构工具箱中的一项工具。我们看到了如何在系统中借助事件在不同的组件之间共享数据,介绍了如何使用事件来帮助我们实现联结以及如何借助事件完成事务。
Randy Shoup在硅谷有着25年的工作经验,他曾经在各种规模的公司中担任高级技术领导和主管,从创业公司,到中型公司,再到eBay和Google这样的大公司,他都有过工作经历。目前,他在位于旧金山的Stitch Fix担任工程VP。他对文化、技术和组织之间的关系研究特别有热情。
Thomas Betts是IHS Markit的首席软件工程师,具有20多年的专业软件开发经验。他一直专注于为客户提供满意的软件解决方案,他曾经在各个行业工作过,包括零售、金融、医疗、国防和旅游等。Thomas目前和他的妻子和儿子住在丹佛,他们喜欢徒步旅行和探索美丽的科罗拉多。