Sql Server 2008R2 数据库发布与订阅 - DBArtist - 博客园

mikel阅读(630)

来源: Sql Server 2008R2 数据库发布与订阅 – DBArtist – 博客园

背景描述:

发布服务器A: (远程端) , 数据库服务名: GUANWANG1

订阅服务器B: (本机)   ,  数据库服务名: PC-LLRDBA

需要从服务器A中数据库发布,然后在B中订阅A发布的数据库;

 

————————————————第一步: 准备工作————————————————

准备工作1: 配置机器名和数据库服务名一致

先检查A,B中的机器名和数据库服务名是否一致,这个很重要!

查看方法:

1.检查SQL Server 的服务器名称

1
2
3
4
use master
go
select @@servername
select serverproperty('servername')

 

如果查出来的两个名字不一样,那就需要把他们的名字改成一样的,方法如下:

2.删除所有之前配置的publishers(’old_server_name’为之前的服务名)

1
2
3
4
5
select from msdb.dbo.MSdistpublishers
DELETE FROM msdb.dbo.MSdistpublishers
select from msdb.dbo.MSdistpublishers
exec sp_droplinkedsrvlogin 'old_server_name',null
exec sp_dropserver 'old_server_name''droplogins'

 

执行完后,重启SQL Server服务

3.配置计算机名与服务名一致

1
2
3
4
5
6
7
8
9
10
USE master
 GO
 if serverproperty('servername') <> @@servername  
begin
       declare @server sysname  
       set   @server = @@servername  
       exec sp_dropserver @server = @server  
       set   @server = cast(serverproperty('servername'as sysname)  
       exec sp_addserver @server = @server , @local 'LOCAL'
end

 

4.查看服务名

1
sp_helpserver

 

 

准备工作2: 配置数据库服务器名称的别称

 

因为数据库发布和订阅,不能用ip登陆,必须用服务名登陆

方法之一是改服务器的登陆别称!

 

首先在本机B配置连接服务器A的登陆别称:

先在[开始],[所有程序]中打开[SQL Server配置管理器]:

 

在[SQL Native Client 10.0配置(32位)]中的别名中配置,如图:

这里的别名必须和服务器B的服务器名一致也就是GUANWANG1,实际的按照自己的名字改动;

端口号是:1433

服务器是对应的服务器机器的IP地址

协议是TCP/IP

 

同理,在下面的[SQL Native Client 10.0配置]中也一样配置下!

 

 

然后在服务器A中也要配置访问本机B的别称,这一步不能少,否则后面本地订阅的时候会失败并报错,”进程无法连接到 Subscriber“PC-LLRDBA”。 “

切记,这里是双向的,当初我查了好久才想起来,服务端也要配置这个……

具体的方法同上:注意别名是本机B的服务名PC-LLRDBA, IP是本机的IP,实在不知道在cmd下config一下就有了!

 

 

 

准备工作好了后,就进行第二步发布

————————————————第二步:发布————————————————

登陆服务器A数据库,用别名登陆:

在[复制],[本地发布]中右键新建发布

 

 

选择你要发布的数据库

 

选择发布类型:(具体选哪种,自己百度下各种的差异),我这里选事务发布

 

可以选择发布的对象(表,存储过程,视图等等),也可以选择部分内容,点开勾选就行了

 

如果没有特别筛选的,就直接下一步(比如说我只要同步Order表中2014年以后的数据,那么你可以点添加,增加筛选条件)

 

选择立即xxx,继续下一步

 

 

输入发布服务器的sa账户登陆密码就行了

 

 

 

 

 

 

OK,到此发布完成,你可以刷新后查看:

 

 

————————————————第三步:订阅————————————————

 

这里选择查找SQL Server发布服务器

 

 

选中需要的订阅的数据库发布:

 

 

 

 

在本地新建一个同名的数据库:

 

 

 

点击与订阅服务器的连接下面的按钮

 

 

 

选择定义计划:

 

 

 

 

 

 

 

 

 

订阅就完成,然后刷新查看内容:

 

 

 

等过一段时间,数据同步完成后就可以查看数据了,是否一致了!

————————————————订阅完成————————————————

 

 

订阅失败案例: 

订阅完成后,却发现本地订阅中没有内容,但是刚刚确实已经订阅成功了啊,为什么呢?

 

然后在发布服务器中,右键本地发布中的数据库,启动复制监视器

 

发现是报错了

 

 

 

这个原因可能有好多种:

1:>>订阅的时候,输入的密码输错了,我第一次就是输成了发布服务器的密码

 

 

2:>>对应的协议要开启

 

 

3:>>对应的服务要开启Sql Server Browser和Sql Server 代理(MSSQLServer)

 

这三个都检查无误了,基本上就搞定了吧!

 

 

还有一种报错是:”对路径”XXXXX”访问被拒绝或者没有访问路径”xxx”的权限”等,这个错误一般是用户的权限不够导致的,解决方法如下:

使用SQL Server发布数据库快照的配置中,如果你选择了使用SQL Server代理,而SQL Server代理服务使用的登陆身份不具有对存放快照文件位置的读写权限时,就会出现该错误。

解决的方法是可以修改SQL Server代理服务的登录身份,具体做法为:程序->Sql Server Configuration Manager->在左边栏选择”SQL Server服务“->在右侧面板中”SQL Server 代理”一行上右击,选择“属性”,在弹出的对话框中选择”内置账户“->LocalSystem即可。

 

 

 

C#生成Guid的几种方式 - Mr.石 - 博客园

mikel阅读(690)

来源: C#生成Guid的几种方式 – Mr.石 – 博客园

1 var uuid = Guid.NewGuid().ToString(); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12

2  var uuidN = Guid.NewGuid().ToString(“N”); // e0a953c3ee6040eaa9fae2b667060e09

3  var uuidD = Guid.NewGuid().ToString(“D”); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12

4  var uuidB = Guid.NewGuid().ToString(“B”); // {734fd453-a4f8-4c5d-9c98-3fe2d7079760}

5  var uuidP = Guid.NewGuid().ToString(“P”); // (ade24d16-db0f-40af-8794-1e08e2040df3)

6  var uuidX = Guid.NewGuid().ToString(“X”); // {0x3fa412e3,0x8356,0x428f,{0xaa,0x34,0xb7,0x40,0xda,0xaf,0x45,0x6f}}

一个人如何完成一家创业公司的技术架构?

mikel阅读(607)

来源: 一个人如何完成一家创业公司的技术架构?

作者 | Anthony N. Simon
译者 | Sambodhi
策划 | 田晓旭
这是一篇长篇阔论的文章,是关于我使用 SaaS 来运行设置的详细介绍,文章会涉及到多方面的内容,包括负载均衡、cron 作业监控、订阅和支付等等。

本文最初发表于作者个人网站,经原作者 Anthony N. Simon 授权,InfoQ 中文站翻译并分享。

虽然文章的标题听起来有些夸张,但我想澄清的是,我们正在讨论的是一家压力不大的个人公司,这家公司是我在德国经营的。自己花钱买的,我很喜欢慢慢来。如果我说“科技创业”,那大概和大多数人想象的不一样。

我不能在没有大量的开源软件和管理服务的情况下做到这一点。我觉得自己就是站在巨人的肩膀上,他们在我之前做过那么多艰苦的工作,我非常感谢他们。

从背景角度来说,我经营的是个人 SaaS,这是我发表的一篇关于我所使用的 技术栈 的文章的详细介绍。在听从我建议之前,先考虑一下你的情况。在技术选择上,你的背景很重要,不需要什么圣杯。

我在 AWS 上使用了 Kubernetes,但是不要陷入需要它的误区。经过一直非常耐心的团队的指导,我花了几年的时间学习了这些工具。因为这是我最擅长的,所以我的工作效率非常高,并且我能将集中精力在运输物品上。你们的目标可能不一样。

闲话少叙,言归正题。

1整体架构

基础设施可以同时处理多个项目,但是为了演示,我将使用 Panelbear,我最近的 SaaS,作为此类设置的一个实例。

图片

Panelbear 中的浏览器计时图表,这是我将在本教程中使用的一个示例项目。

就技术而言,该 SaaS 每秒处理来自世界各地的大量请求,并以高效的格式存储数据,实现实时查询。

就业务而言,它仍处于起步阶段(我是半年前推出的),但它的发展比我预期的要快,特别是我最初为自己创建的 Django 应用,它是在一个小的虚拟专用服务器上使用 SQLite。对我当时的目标而言,这是非常有效的,而且我可能已经将这一模式推进了很远。

但是,我变得越来越沮丧,不得不重新使用许多我已经习惯了的工具:零停机部署、自动缩放、健康检查、自动 DNS / TLS / ingress 规则等等。Kubernetes 宠坏了我,让我习惯于在保持控制力和灵活性的情况下处理更高级抽象。

快进六个月,经历了几次迭代,虽然我目前的设置仍然是 Django 的单体版本,我现在将 Postgres 用作应用数据库,ClickHouse 用作分析数据,Redis 用作缓存。同时使用 Celery 来调度任务,使用自定义事件队列来缓冲写操作。这其中大部分都在托管的 Kubernetes 集群上运行。

图片

架构概述

听起来似乎很复杂,但它实际上是老式的单体架构,运行在 Kubernetes 上。如果把 Django 换成 Rails 或 Laravel,你就知道我在说什么了。令人感兴趣的是,如何将所有的东西粘合在一起并自动执行:自动缩放、入口、TLS 证书、故障转移、日志、监控,等等。

值得一提的是,我在多个项目中都使用了这种设置,它帮助我降低了成本,而且很容易进行实验(编写 Dockerfile 和 git push)。因为经常有人问我这样一个问题:和你想的相反,我实际上花了很少的时间去管理基础设施,通常每个月要花大概 0~2 小时。大部分时间都用来开发功能、做客户支持,以及拓展业务。

话又说回来,这些工具我都用了好几年了,都很熟悉。虽然我的设置对它们的能力来说很简单,但我的日常工作却花了很多年才做到这一点。因此,我不会说这就是“阳光和玫瑰”。

不知道谁先说了这句话,但我这样对朋友们说:“Kubernetes 让简单的东西变得复杂,但也让复杂的东西变得简单”。

2自动 DNS、SSL 和负载均衡

既然你已经了解了我在 AWS 上托管的 Kubernetes 集群,并且在其中运行了各种项目,那么让我们进入本文的第一站:如何将流量引入集群。

我的集群是在一个私有网络中,因此你无法从公共互联网中直接访问。有几个部分可以控制集群的访问和负载均衡流量。

基本上,我让 Cloudflare 将所有流量代理到 NLB(AWS L4 Network Load Balancer,网络负载均衡器)。该负载均衡器是公共互联网和我的私有网络之间的桥梁。当收到请求后,它将转发给其中一个 Kubernetes 集群节点。这些节点分布在 AWS 中多个可用性区域的私有子网中。所有这些都是顺便处理的,但以后还会有更多。

图片

流量被缓存在边缘,或者转发到我运营的 AWS 区域中

ingress-nginx 就是这样做的:“Kubernetes 如何知道该将请求转发到哪个服务?”简单地说,它是一个 NGINX 集群,由 Kubernetes 管理,是集群内所有流量的入口。

在将请求发送到相应的应用程序容器之前,NIGIX 适用速度限制和其他流量形成规则。就 Panelbear 而言,应用容器是由 Uvicorn 服务的 Django。

这种方法与传统的 nginx/gunicorn/Django 的 VPS 方式没有什么不同,它带来了横向扩展和自动设置 CDN 的优点。它还可以“一次设置就忘记”,在 Terraform/Kubernetes 之间主要有一些文件,由所有已部署项目共享。

在部署新项目时,入口配置基本上只有 20 行代码,就像这样:

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:namespace: examplename: example-apiannotations:kubernetes.io/ingress.class: "nginx"nginx.ingress.kubernetes.io/limit-rpm: "5000"cert-manager.io/cluster-issuer: "letsencrypt-prod"external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"spec:tls:- hosts:- api.example.comsecretName: example-api-tlsrules:- host: api.example.comhttp:paths:- path: /backend:serviceName: example-apiservicePort: http

这些注释描述了我想要一个 DNS 记录,流量由 Cloudflare 代理,通过 Letsencrypt 获取 TLS 证书,并且应该根据 IP 限制每分钟的请求率,然后再将请求转发给我的应用。

Kubernetes 负责让 infra 中的这些改变反映出期望的状态。尽管有点儿啰嗦,但在实践中效果还不错。

3自动部署和回滚

图片

推送新提交时发生的操作链

无论何时我想要掌握一个项目,它都会在 GitHub Actions 上启动一个 CI 管道。此管道运行一些代码库检查和端到端测试(使用 Docker compose 来设置整个环境),这些检查通过后,将创建一个新的 Docker 镜像,并将其推送到 ECR(AWS 中的 Docker 注册表)。

就应用仓库而言,新版本的应用已经过测试,可以作为 Docker 镜像部署。

panelbear/panelbear-webserver:6a54bb3

“那么接下来呢?Docker 有新的镜像,但是还没有部署?” Kubernetes 集群有一个叫做 Flux 的组件,可以自动保存集群中当前运行的内容以及我的应用最新的镜像同步。

图片

在我的基础设施单体仓库中,Flux 自动跟踪新版本

Flux 会在 Docker 的新镜像可用时自动触发增量推出,并将这些操作记录到“基础设施单体仓库”中。

我希望有一个版本控制的基础设施,这样每当我在 Terraform 和 Kubernetes 之间的这个仓库中有新的提交时,它们就可以对 AWS、Cloudflare 和其他服务进行必要的修改,使我的仓库状态和部署的内容保持一致。

其所有版本都受版本控制,并且每次部署都有线性历史。这就是说,多年来,由于我没有在一些晦涩难懂的用户界面上通过点击配置神奇的设置,所以我需要记住的东西更少。

此单体仓库可作为可部署文档,稍后将详细讨论。

4任其崩溃

几年前,我用 Actor 并发模型在公司的多个项目中工作,并爱上了其生态系统中的许多想法。一发不可收拾,我很快开始阅读关于 Erlang 的书,以及它所阐述的让事情崩溃的哲学。

也许我已经把这个想法做了很多扩展,但是在 Kubernetes 里,我喜欢使用存活探针(liveliness probes)和自动重启来达到同样的效果。

摘自 Kubernetes 文档:“kubelet 使用存活探针来知道何时重启容器。例如,存活探针可以捕获到一个死锁,即应用程序正在运行,但无法取得进展。在这样的状态下重启容器有助于使应用更可用,尽管有 bug。”

在实践中,这对我来说很管用。容器和节点本来就是来来往往的,而 Kubernetes 则在“治疗”不健康的 pod(更像是杀掉)的同时,优雅地将流量转移到健康的 pod 上。残忍,但有效。

5横向自动缩放

根据 CPU / 内存使用情况,我的应用容器会自动缩放。Kubernetes 将尽可能多的工作负载打包到每个节点上,以便最大限度地利用它。

如果集群中每个节点的 pod 过多,它将自动生成更多的服务器,以增加集群容量并减轻负载。类似地,当事情不多的时候,它就会缩减规模。

以下是 Horizontal Pod Autoscaler 的样子:

apiVersion: autoscaling/v1kind: HorizontalPodAutoscalermetadata:name: panelbear-apinamespace: panelbearspec:scaleTargetRef:apiVersion: apps/v1kind: Deploymentname: panelbear-apiminReplicas: 2maxReplicas: 8targetCPUUtilizationPercentage: 50

在本例中,它会根据 CPU 的使用情况自动调整panelbear-api pods的数量,从 2 个副本开始,但上限为 8 个。

6CDN 缓存的静态资产

在为我的应用定义入口规则时,注释cloudflare-proxied: "true"就是告诉 Kubernetes 我要将 Cloudflare 用作 DNS,并通过其 CDN 和 DDoS 保护来代理所有请求。

从此以后,使用它将变得非常简单。在应用中,我只需设置标准的 HTTP 缓存头来指定哪些请求可以被缓存以及缓存多长时间。

# Cache this response for 5 minutesresponse["Cache-Control"] = "public, max-age=300"

在边缘服务器上,Cloudflare 将使用这些响应头来控制缓存行为。对这种简单的设置来说,效果非常好。

通过 Whitenoise,我可以从应用容器直接为静态文件提供服务,因此可以避免每次部署时将静态文件上传到 Nginx/Cloudfront/S3。迄今为止,它运行得很好,并且 CDN 在填充大部分请求时将对它们进行缓存。其性能非常出色,并且使事情更加简单。

在一些静态网站上,我也使用 NextJS,例如 Panelbear 的登陆页面。通过 Cloudfront/S3 甚至 Netlify 或 Vercel,我可以为它提供服务,但是只需将其作为集群中的一个容器运行,当请求静态资产时,Cloudflare 可以轻松地缓存它们。对于我来说,这样做的额外成本为零,并且我可以重复地使用所有的工具来部署、日志记录和监控。

7应用数据缓存

除了静态文件缓存外,还有应用数据缓存(例如重型计算结果、Django 模型、限速计数器等)。

我利用了内存中的缓存文档置换机制 将频繁访问的对象保存在内存中,并且没有网络调用(纯 Python,不涉及 Redis),这对我有好处。

然而,大多数端点只是在集群中使用 Redis 来缓存。其速度仍然很快,并且缓存的数据可以被所有的 Django 实例共享,即使在重新部署之后,当内存中的缓存被删除时,这些数据可以可以被共享。

下面是一个实际例子:

我的定价计划是基于每月的事件分析。为实现这一目标,需要进行某种计量,以了解在当前的账单期间消耗了多少事件,并实施限制。但是,我不会在客户超过限额后立即中断服务。取而代之的是,将自动发送一封“耗尽容量”的电子邮件,并在 API 开始拒绝新数据之前为客户提供宽限期。

这样客户就有足够的时间在确保数据不丢失的情况下决定升级对他们是否有意义。举例来说,在流量高峰期,如果他们的内容被病毒式传播,或者他们只享受周末,而不去查阅邮件。若客户决定继续使用目前的计划而不进行升级,则不会有任何损失,且在使用量回到计划限制范围后,一切将恢复正常。

因此,为了实现这一功能,我使用了一个函数,应用了上面的规则,它需要多次调用数据库和 ClickHouse,但是会缓存 15 分钟,以避免在每次请求时重新计算它们。这样做很好,也很简单。值得注意的是:计划变更时缓存将失效,否则升级将在 15 分钟后生效。

@cache(ttl=60 * 15)def has_enough_capacity(site: Site) -> bool:"""Returns True if a Site has enough capacity to accept incoming events,or False if it already went over the plan limits, and the grace period is over."""
8单个端点速率限制

尽管我在 Kubernetes 的 nginx-ingress 上执行全局速率限制,但是有时候我想要更具体地限制每个端点 / 方法。

为了实现这一点,我使用了优秀的 Django Ratelimit 库为每个 Django 视图轻松声明限制。其配置为使用 Redis 作为后端,以跟踪向每个端点发出请求的客户端(它存储的是基于客户端密钥的哈希值,而不是基于 IP)。

例如:

class MySensitiveActionView(RatelimitMixin, LoginRequiredMixin):ratelimit_key = "user_or_ip"ratelimit_rate = "5/m"ratelimit_method = "POST"ratelimit_block = Truedef get():...def post():...

在上面的例子中,如果客户端每分钟试图向此特定端点发送 5 次以上 POST,则会以HTTP 429 Too Many Requests状态码拒绝后续调用。

图片

当速率受限时,会收到友好的错误消息

9应用管理

Django 免费为我所有的模型提供了一个管理面板。它是内置的,而且对于随时检查客户支持工作的数据非常方便。

图片

Django 的内置管理面板对于随时提供客户支持非常有用

在用户界面上,我添加了动作来帮助我管理事情。例如,阻止对可疑账户的访问,发送公告邮件,以及请求批准完全删除账户(首先是软删除,72 小时后彻底销毁)。

安全性方面:只有员工用户能够访问面板(我),为提高安全性,我打算在所有账户上添加 2FA。

另外,每一次用户登录,我都会自动将包含新会话详情的安全邮件发送到该账户邮箱。我将在每次新登陆时发送,但将来我可能会更改此操作,以跳过已知设备。它并非很“MVP 的功能”,但是我关注安全性,并且添加它也不复杂。最起码如果有人登录了我的账户,我会收到警告。

当然,对应用的强化内容远不止这些,但这不在本文的讨论范畴。

图片

登陆时可能收到的安全活动电子邮件示例

10运行计划作业

另外一个有趣的用例是,我在 SaaS 中运行了许多不同的计划工作。例如,为我的客户生成每日报告,每 15 分钟计算一次使用情况统计,给员工发邮件(我每天都会收到包含最重要指标的邮件)等等。

这个设置实际上很简单,我在集群中只运行了几个 Celery worker 和一个 Celery beat 调度器。它们被配置为将 Redis 用作任务队列。我花了一个下午的时间设置了一次,幸运的是,到目前为止,我还没有遇到任何问题。

当计划任务未按预期运行时,我希望通过 SMS/Slack/Email 获得通知。例如,当每周报告任务被卡住或明显延迟时。为此,我使用了 Healthchecks.io,但是你也可以看看 Cronitor 和 CronHub,因为我也听到了一些关于它们的好消息。

图片

来自 Healthchecks.io 的 cron 作业监视指示板

我编写了一小段 Python 代码,对其 API 进行抽象,以自动创建监视程序和状态 ping:

def some_hourly_job():# Task logic...# Ping monitoring service once task completesTaskMonitor(name="send_quota_depleted_email",expected_schedule=timedelta(hours=1),grace_period=timedelta(hours=2),).ping()
11应用配置

全部应用都是通过环境变量来配置的,虽然已经过时,但是具有良好的可移植性和支持性。举例来说,在 Djangosettings.py文件中,我会为默认值设置变量。

INVITE_ONLY = env.str("INVITE_ONLY", default=False)

像下面这样在代码中的任意位置使用:

from django.conf import settings# If invite-only, then disable account creation endpointsif settings.INVITE_ONLY:...

在 Kubernetesconfigmap中,我可以覆盖以下环境变量:

apiVersion: v1kind: ConfigMapmetadata:namespace: panelbearname: panelbear-webserver-configdata:INVITE_ONLY: "True"DEFAULT_FROM_EMAIL: "The Panelbear Team <support@panelbear.com>"SESSION_COOKIE_SECURE: "True"SECURE_HSTS_PRELOAD: "True"SECURE_SSL_REDIRECT: "True"
12保守秘密

处理秘密的方式非常有趣,我想把它们和其他配置文件一起提交到我的基础设施仓库,但秘密应该被加密。

为了实现这个目标,我在 Kubernetes 中使用了 kubeseal。这个组件使用非对称加密技术对我的秘密进行加密,并且只有获得授权可以访问解密密钥的集群才能对其进行解密。

举例来说,你可能会在我的基础设施仓库中发现以下内容:

apiVersion: bitnami.com/v1alpha1kind: SealedSecretmetadata:name: panelbear-secretsnamespace: panelbearspec:encryptedData:DATABASE_CONN_URL: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...SESSION_COOKIE_SECRET: oi7ySY1ZA9rO43cGDEq+ygByri4OJBlK...

集群将自动解密秘密,并将其作为环境变量传递给相应的容器:

DATABASE_CONN_URL='postgres://user:pass@my-rds-db:5432/db'SESSION_COOKIE_SECRET='this-is-supposed-to-be-very-secret'

为了在集群中保护秘密,我使用 AWS 通过 KMS 管理的加密密钥定期轮换。当创建 Kubernetes 集群时,这是一个单独的设置,并且可以完全管理。

就操作而言,这意味着我将秘密作为环境变量写入 Kubernetes manifests,然后运行一个命令对 Kubernetes manifests 进行加密,然后在提交前推送更改。

只需几秒钟就可以部署秘密,在运行我的容器前,集群会负责自动解密。

13关系数据:Postgres

为了进行实验,我在集群内运行一个普通的 Postgres 容器,以及一个每天备份到 S3 的 Kubernetes cronjob。这样可以帮助我节省开支,而且对于新手来说,也很简单。

不过,随着 Panelbear 等项目的发展,我会把数据库从集群中转移到 RDS,让 AWS 负责加密备份、安全更新以及所有其他无聊的事情。

为提高安全性,AWS 管理的数据库仍在私有网络中部署,因此不能通过公共互联网访问。

14列式数据:ClickHouse

在 Panelbear 中,我依靠 ClickHouse 有效地存储分析数据和(软)实时查询。它是一种神奇的列式数据库,其速度惊人,并且当你的数据结构化得很好时,你可以获得高压缩比(更小的存储成本 = 更高的利润)。

当前,我在 Kubernetes 集群中自行托管一个 ClickHouse 实例。我用一个由 AWS 管理的带有加密卷密钥的 StatefulSet。我有一个 Kubernetes CronJob,它定期以高效列式格式将所有数据备份到 S3. 对于灾难恢复,我有几个脚本用于在 S3 中手动备份和恢复数据。

直到现在,ClickHouse 仍然坚不可摧,它是一款令人印象深刻的软件。当我开始使用 SaaS 时,这是我唯一不熟悉的工具,但是由于他们的文档,我能够很快上手运行。

如果想要挤出更多的性能(例如,优化字段类型以获得更好的压缩、预计算物化表以及优化实例类型), 我认为这是一个容易实现的方法,但是现在它已经足够好了。

15基于 DNS 的服务发现

除了 Django 之外,我还为 Redis、ClickHouse、NextJS 等运行容器。这些容器必须以某种方式相互通信,而这是通过 Kubernetes 中内置的服务发现实现的。

这很简单。我为容器定义了一个服务资源,Kubernetes 会自动管理集群内的 DNS 记录,并将流量路由到相应的服务。

举例来说,给定集群中公开的 Redis 服务:

apiVersion: v1kind: Servicemetadata:name: redisnamespace: weekend-projectlabels:app: redisspec:type: ClusterIPports:- port: 6379selector:app: redis

通过以下 URL,我可以从集群的任何地方访问这个 Redis 实例:

redis://redis.weekend-project.svc.cluster:6379

注意,URL 包含服务名称和项目名称空间。这样,你的所有集群服务就可以轻松地彼此通信,无论它们运行在集群的哪个位置。

例如,下面是如何通过环境变量配置 Django 在集群内使用 Redis:

apiVersion: v1kind: ConfigMapmetadata:name: panelbear-confignamespace: panelbeardata:CACHE_URL: "redis://redis.panelbear.svc.cluster:6379/0"ENV: "production"...

即使容器在自动缩放期间跨节点移动, Kubernetes 也会自动使 DNS 记录与正常 pod 保持同步。这个背后的工作方式很有趣,但是超出了本文的讨论范围。

16版本控制的基础设施

我想要的是版本控制的、可复制的基础设施,可以使用一些简单的命令来创建和销毁它。

为了实现这一点,我在一个单体仓库中使用 Docker、Terraform 和 Kubernetes manifests,包含了所有的基础设施,甚至跨多个项目。我还将为每个应用 / 项目使用一个单独的 git repo,但是这段代码并不知道它将在什么环境中运行。

如果你熟悉 12-Factor,这种分离可能会让你想起一两件事情。从本质上讲,我的应用并不知道它将要运行的具体基础设施,而是通过环境变量来配置。

通过在 git repo 中描述我的基础设施,我就不需要在一些晦涩的用户界面中跟踪每一个小资源和配置设置。这样,当灾难恢复时,我可以使用一条命令来恢复我的整个栈。

下面是一个文件夹结构的例子,你可以在下文的单体仓库上找到:

# Cloud resourcesterraform/aws/rds.tfecr.tfeks.tflambda.tfs3.tfroles.tfvpc.tfcloudflare/projects.tf# Kubernetes manifestsmanifests/cluster/ingress-nginx/external-dns/certmanager/monitoring/apps/panelbear/webserver.yamlcelery-scheduler.yamlcelery-workers.yamlsecrets.encrypted.yamlingress.yamlredis.yamlclickhouse.yamlanother-saas/my-weekend-project/some-ghost-blog/# Python scripts for disaster recovery, and CItasks/...# In case of a fire, some help for future meREADME.mdDISASTER.mdTROUBLESHOOTING.md

这种设置的另一个好处是,所有的移动件都在同一位置描述。我可以配置和管理可重用的组件,比如集中式日志、应用监控和加密秘密等等。

17用于云资源的 Terraform

我使用 Terraform 来管理大部分的基础云资源。它有助于我记录和跟踪构成基础设施的资源和配置。如果发生灾难恢复,我可以使用一个命令来启动和回滚资源。

举例来说,这里有一个 Terraform 文件,用于为 30 天后过期的加密备份创建私有 S3 存储桶:

resource "aws_s3_bucket" "panelbear_app" {bucket = "panelbear-app"acl    = "private"tags = {Name        = "panelbear-app"Environment = "production"}lifecycle_rule {id      = "backups"enabled = trueprefix  = "backups/"expiration {days = 30}}server_side_encryption_configuration {rule {apply_server_side_encryption_by_default {sse_algorithm     = "AES256"}}}}
18用于 App 部署的 Kubernetes manifest

我所有的 Kubernetes manifests 也被描述在基础设施单体仓库的 YAML 文件中。我把它们分成了两个目录:clusterapps

我在cluster群目录中描述了所有集群范围内的服务和配置,比如 nginx-ingress,加密秘密,prometheus scrapers 等等。基本上是可重用位。

而在apps目录中,每个项目都包含一个命名空间,描述了部署它所需要的内容(入口规则、部署、秘密、卷等)。

Kubernetes 最酷的功能之一就是,你可以自定义栈中的任何东西。例如,如果我希望使用可调整大小的加密 SSD 卷,那么可以在集群中定义新的“StorageClass”。Kubernetes 以及在这种情况下,AWS 会协调和实现神奇的东西。例如:

apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:name: encrypted-ssdprovisioner: kubernetes.io/aws-ebsparameters:type: gp2encrypted: "true"reclaimPolicy: RetainallowVolumeExpansion: truevolumeBindingMode: WaitForFirstConsumer

既然我可以继续将这种持久性存储附加到任何部署中,Kubernetes 就会为我管理请求的资源:

# Somewhere in the ClickHouse StatefulSet configuration...storageClassName: encrypted-ssdresources:requests:storage: 250Gi...
19订阅和支付

我用 Stripe Checkout 来保存所有工作,包括处理付款、创建结账屏幕、处理信用卡 3D 安全要求,甚至客户账单门户。

我无法接触到支付信息本身,这极大地减轻了我的负担,使我能够集中精力放在我的产品上,而非信用卡处理和反欺诈等高度敏感的问题上。

图片

Panelbear 中的客户计费门户示例

我们只需要创建一个新的客户会话,然后将客户重定向到 Stripe 的托管页面。接着我监听 webhook,了解客户是否升级 / 降级 / 取消,并相应地更新数据库。

这其中肯定有一些重要的内容,比如确认 webhook 确实来自 Stripe(你必须用秘密验证请求签名),但是 Stripe 的文档非常好地涵盖了所有的内容。

只有很少的计划,所以很容易在代码库中管理它们。基本上我是这么想的:

# Plan constantsFREE = Plan(code='free',display_name='Free Plan',features={'abc', 'xyz'},monthly_usage_limit=5e3,max_alerts=1,stripe_price_id='...',)BASIC = Plan(code='basic',display_name='Basic Plan',features={'abc', 'xyz'},monthly_usage_limit=50e3,max_alerts=5,stripe_price_id='...',)PREMIUM = Plan(code='premium',display_name='Premium Plan',features={'abc', 'xyz', 'special-feature'},monthly_usage_limit=250e3,max_alerts=25,stripe_price_id='...',)# Helpers for easy accessALL_PLANS = [FREE, BASIC, PREMIUM]PLANS_BY_CODE = {p.code: p for p in ALL_PLANS}

之后,我可以将它用于任何 API 端点、cron 作业和管理任务,以确定那些限制 / 功能适合特定的客户。在BillingProfile模型中,给定客户的当前计划是称为plan_code的列。由于我计划在某些时候添加组织 / 团队,因此我将把BillingProfile迁移到账户所有者 / 管理用户,所以我将用户和账单信息分离。

如果你在一家电商里提供成千上万的单品,那么这个模式是不能被扩展的,但是对于我来说,这个模式非常有效,因为 SaaS 通常只有几个计划。

20日志

无需使用任何日志代理或类似工具来检测代码。只要将日志记录到 stdout,Kubernetes 就会自动收集,然后对日志进行循环更新。还可以使用 FluentBit 自动地将这些日志发送到像 Elasticsearch/Kibana 这样的地方,但是为了简单起见,我还没有这样做。

我用 Stern 检查日志,这是用于 Kubernetes 的 一个小 CLI 工具,它可以非常轻松地跟踪多个 pod 应用日志。举例来说,stern-ningress-nginx可以跟踪我的 nginxpods 访问日志,甚至在多个节点之间进行跟踪。

21监控和警报

起初,我使用自托管的 Prometheus/Grafana 来自动监控集群和应用指标。但是,我觉得自托管我的监控栈并不舒服,因为如果集群中发生了什么问题,我的警报系统也会随之瘫痪(不太好)。

如果说有什么东西是绝对不能坏的,那就是你的监控系统,否则你基本上就是在没有仪器的情况下飞行。这就是为什么我把监控 / 警报系统改为托管服务(New Relic)。

所有我的服务都有一个 Prometheus 集成,能够自动记录和转发指标到兼容的后端,如 Datadog、New Relic、Grafana Cloud 或自托管的 Prometheus 实例(我曾经做过)。为了迁移到 New Relic,我需要做的就是使用他们的 Prometheus Docker 镜像,然后关闭自托管的监控栈。

图片

New Relic 仪表盘示例,包含最重要的统计数据摘要

图片

使用 New Relic 的探针监测世界各地的正常运行时间

从自托管的 Grafana/Loki/Prometheus 栈迁移到 New Relic,减少了我的操作面。更重要的是,即使我的 AWS 区域宕机了,我仍然会收到警报。

你也许想知道我是如何从 Django 应用中公开指标的。在我的应用中,我利用了优秀的 django-prometheus 库来简单地注册一个新的计数器 / 仪表。

from prometheus_client import CounterEVENTS_WRITTEN = Counter("events_total","Total number of events written to the eventstore")# We can increment the counter to record the number of events# being written to the eventstore (ClickHouse)EVENTS_WRITTEN.incr(count)

这会在服务器的/metrics端点中公开该指标和其他指标(仅在集群内可访问)。Prometheus 每分钟都会自动抓取该端点,并将指标转发给 New Relic。

图片

由于 Prometheus 的集成,该指标会自动显示在 New Relic 中

22错误跟踪

人人都认为在他们的应用中没有错误,除非开始错误跟踪。异常太容易在日志中丢失,或者更糟糕的是,你意识到了它,但由于缺乏上下文,无法重现问题。

用 Sentry 来汇总整个应用中的错误并通知我。检测 Django 应用非常简单:

SENTRY_DSN = env.str("SENTRY_DSN", default=None)# Init Sentry if configuredif SENTRY_DSN:sentry_sdk.init(dsn=SENTRY_DSN,integrations=[DjangoIntegration(), RedisIntegration(), CeleryIntegration()],# Do not send user PII data to Sentry# See also inbound rules for special patternssend_default_pii=False,# Only sample a small amount of performance tracestraces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.008),)

这很有用,因为它在异常发生时自动收集一堆上下文信息:

图片

当发生异常情况时,Sentry 汇总并通知我

通过 Slack 的 #alerts 频道,我可以集中所有的警报:宕机时间、cron 作业失败、安全警报、性能下降、应用异常等等。这样做非常好,因为当多个服务在同一时间向我发出看似不相关的问题的警告时,我就能把问题关联起来。

图片

Slack 警报示例,图为 CDN 端点在澳大利亚悉尼宕机

23资料收集与其他好处

在需要深入研究时,我还将使用诸如 cProfile 和 snakeviz 这样的工具,以更好地了解有关应用性能的分配、调用次数和其他统计数据。这听起来很花哨,但是它们是非常容易使用的工具,而且在过去帮助我找出各种问题,这些问题使我的仪表盘因看似不相关的代码而变慢。

图片

cProfile 和 snakeviz 是很好的工具,可以在本地对 Python 代码进行配置文件。

在本地机器上,我还使用 Django Debug Toolbar 轻松地检查视图触发的查询,在开发期间预览发送的电子邮件,以及其他一些好处。

图片

Django 的 Debug 工具栏对于检查本地开发和预览事务性电子邮件非常有用

24结语

如果你看到这里,我希望你喜欢这篇文章。它最终比我最初计划的要长很多,因为有很多地方要涉及。

如果你还不熟悉这些工具,可以考虑先使用一个托管平台,比如 Render 或 DigitalOcean 的 App Platform(无利益相关,只是听说这两个平台很不错)。它们可以帮助你把精力集中在产品上,而且还能得到我在本文提到的好处。

“你是不是什么都用 Kubernetes?” 不是的,不同的项目有不同的需求。比如这个博客就是托管在 Vercel 上的。

有趣的是,我花在写这篇文章上的时间比实际设置我所描述的一切还要多。9000 多字,几周的工作断断续续,很显然,我是个写得慢的人。

话虽如此,我确实打算写更多的后续文章,介绍一些具体的技巧和诀窍,并分享更多一路走来的经验教训。特别是作为一名工程师,我有很多需要学习的地方。

作者介绍

Anthony N. Simon,Stylight 工程师,Panelbear 创始人。

原文链接

https://anthonynsimon.com/blog/one-man-saas-architecture/

从零开始搭建创业公司后台技术栈

mikel阅读(705)

来源: 从零开始搭建创业公司后台技术栈

前言

说到后台技术栈,脑海中是不是浮现的是这样一幅图?

图片

图 1

有点眼晕,以下只是我们会用到的一些语言的合集,而且只是语言层面的一部分,就整个后台技术栈来说,这只是一个开始,从语言开始,还有很多很多的内容。今天要说的后台是大后台的概念,放在服务器上的东西都属于后台的东西,比如使用的框架,语言,数据库,服务,操作系统等等。

整个后台技术栈我的理解包括 4 个层面的内容:

  • 语言:用了哪些开发语言,如:C++/Java/Go/PHP/Python/Ruby 等等;
  • 组件:用了哪些组件,如:MQ 组件,数据库组件等等;
  • 流程:怎样的流程和规范,如:开发流程,项目流程,发布流程,监控告警流程,代码规范等等;
  • 系统:系统化建设,上面的流程需要有系统来保证,如:规范发布流程的发布系统,代码管理系统等等;

结合以上的的 4 个层面的内容,整个后台技术栈的结构如图 2 所示:

图片

图2 后台技术栈结构

以上的这些内容都需要我们从零开始搭建,在创业公司,没有大公司那些完善的基础设施,需要我们从开源界,从云服务商甚至有些需要自己去组合,去拼装,去开发一个适合自己的组件或系统以达成我们的目标。咱们一个个系统和组件的做选型,最终形成我们的后台技术栈。

各系统组件选型

1、项目管理/Bug管理/问题管理

项目管理软件是整个业务的需求,问题,流程等等的集中地,大家的跨部门沟通协同大多依赖于项目管理工具。有一些 SaaS 的项目管理服务可以使用,但是很多时间不满足需求,此时我们可以选择一些开源的项目,这些项目本身有一定的定制能力,有丰富的插件可以使用,一般的创业公司需求基本上都能得到满足,常用的项目如下:

  • Redmine:用 Ruby 开发的,有较多的插件可以使用,能自定义字段,集成了项目管理,Bug 问题跟踪,WIKI 等功能,不过好多插件 N 年没有更新了;
  • Phabricator:用 PHP 开发的,Facebook 之前的内部工具,开发这工具的哥们离职后自己搞了一个公司专门做这个软件,集成了代码托管, Code Review,任务管理,文档管理,问题跟踪等功能,强烈推荐较敏捷的团队使用;
  • Jira:用 Java 开发的,有用户故事,task 拆分,燃尽图等等,可以做项目管理,也可以应用于跨部门沟通场景,较强大;
  • 悟空 CRM :这个不是项目管理,这个是客户管理,之所以在这里提出来,是因为在 To B 的创业公司里面,往往是以客户为核心来做事情的,可以将项目管理和问题跟进的在悟空 CRM 上面来做,他的开源版本已经基本实现了 CR< 的核心 功能,还带有一个任务管理功能,用于问题跟进,不过用这个的话,还是需要另一个项目管理的软件协助,顺便说一嘴,这个系统的代码写得很难维护,只能适用于客户规模小(1万以内)时。
2、DNS

DNS 是一个很通用的服务,创业公司基本上选择一个合适的云厂商就行了,国内主要是两家:

  • 阿里万网:阿里 2014 年收购了万网,整合了其域名服务,最终形成了现在的阿里万网,其中就包含 DNS 这块的服务;
  • 腾讯 DNSPod:腾讯 2012 年以 4000 万收购 DNSPod 100% 股份,主要提供域名解析和一些防护功能;

如果你的业务是在国内,主要就是这两家,选 一个就好,像今日头条这样的企业用的也是 DNSPod 的服务,除非一些特殊的原因才需要自建,比如一些 CDN 厂商,或者对区域有特殊限制的。要实惠一点用阿里最便宜的基础版就好了,要成功率高一些,还是用 DNSPod 的贵的那种。

在国外还是选择亚马逊吧,阿里的 DNS 服务只有在日本和美国有节点,东南亚最近才开始部点, DNSPod 也只有美国和日本,像一些出海的企业,其选择的云服务基本都是亚马逊。

如果是线上产品,DNS 强烈建议用付费版,阿里的那几十块钱的付费版基本可以满足需求。如果还需要一些按省份或按区域调试的逻辑,则需要加钱,一年也就几百块,省钱省力。

如果是国外,优先选择亚马逊,如果需要国内外互通并且有自己的 APP 的话,建议还是自己实现一些容灾逻辑或者智能调度,因为没有一个现成的 DNS 服务能同时较好的满足国内外场景,或者用多个域名,不同的域名走不同的 DNS 。

3、LB(负载均衡)

LB(负载均衡)是一个通用服务,一般云厂商的 LB 服务基本都会如下功能:

  • 支持四层协议请求(包括 TCP、UDP 协议);
  • 支持七层协议请求(包括 HTTP、HTTPS 协议);
  • 集中化的证书管理系统支持 HTTPS 协议;
  • 健康检查;

如果你线上的服务机器都是用的云服务,并且是在同一个云服务商的话,可以直接使用云服务商提供的 LB 服务,如阿里云的 SLB,腾讯云的 CLB,亚马逊的 ELB 等等。如果是自建机房基本都是 LVS + Nginx。

4、CDN

CDN 现在已经是一个很红很红的市场,基本上只能挣一些辛苦钱,都是贴着成本在卖。国内以网宿为龙头,他们家占据整个国内市场份额的 40% 以上,后面就是腾讯,阿里。网宿有很大一部分是因为直播的兴起而崛起。

国外,Amazon 和 Akamai 合起来占比大概在 50%,曾经的国际市场老大 Akamai 拥有全球超一半的份额,在 Amazon CDN入局后,份额跌去了将近 20%,众多中小企业都转向后者,Akamai 也是无能为力。

国内出海的 CDN 厂商,更多的是为国内的出海企业服务,三家大一点的 CDN 服务商里面也就网宿的节点多一些,但是也多不了多少。阿里和腾讯还处于前期阶段,仅少部分国家有节点。

就创业公司来说,CDN 用腾讯云或阿里云即可,其相关系统较完善,能轻松接入,网宿在系统支持层面相对较弱一些,而且还贵一些。并且,当流量上来后,CDN 不能只用一家,需要用多家,不同的 CDN 在全国的节点覆盖不一样,而且针对不同的客户云厂商内部有些区分客户集群,并不是全节点覆盖(但有些云厂商说自己是全网节点),除了节点覆盖的问题,多 CDN 也在一定程度上起到容灾的作用。

5、RPC 框架

维基百科对 RPC 的定义是:远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。

通俗来讲,一个完整的 RPC 调用过程,就是 Server 端实现了一个函数,客户端使用 RPC 框架提供的接口,调用这个函数的实现,并获取返回值的过程。

业界 RPC 框架大致分为两大流派,一种侧重跨语言调用,另一种是偏重服务治理。

跨语言调用型的 RPC 框架有 Thrift、gRPC、Hessian、Hprose 等。这类 RPC 框架侧重于服务的跨语言调用,能够支持大部分的语言进行语言无关的调用,非常适合多语言调用场景。但这类框架没有服务发现相关机制,实际使用时需要代理层进行请求转发和负载均衡策略控制。

其中,gRPC 是 Google 开发的高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。本身它不是分布式的,所以要实现框架的功能需要进一步的开发。

Hprose(High Performance Remote Object Service Engine)是一个 MIT 开源许可的新型轻量级跨语言跨平台的面向对象的高性能远程动态通讯中间件。

服务治理型的 RPC 框架的特点是功能丰富,提供高性能的远程调用、服务发现及服务治理能力,适用于大型服务的服务解耦及服务治理,对于特定语言(Java)的项目可以实现透明化接入。缺点是语言耦合度较高,跨语言支持难度较大。国内常见的冶理型 RPC 框架如下:

  • Dubbo:Dubbo 是阿里巴巴公司开源的一个 Java 高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring 框架无缝集成。当年在淘宝内部,Dubbo 由于跟淘宝另一个类似的框架 HSF 有竞争关系,导致 Dubbo 团队解散,最近又活过来了,有专职同学投入。
  • DubboX:DubboX 是由当当在基于 Dubbo 框架扩展的一个 RPC 框架,支持 REST 风格的远程调用、Kryo/FST 序列化,增加了一些新的feature。Motan:Motan 是新浪微博开源的一个 Java 框架。它诞生的比较晚,起于 2013 年,2016 年 5 月开源。Motan 在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。
  • rpcx:rpcx 是一个类似阿里巴巴 Dubbo 和微博 Motan 的分布式的 RPC 服务框架,基于 Golang net/rpc 实现。但是 rpcx 基本只有一个人在维护,没有完善的社区,使用前要慎重,之前做 Golang 的 RPC 选型时也有考虑这个,最终还是放弃了,选择了 gRPC,如果想自己自研一个 RPC 框架,可以参考学习一下。
6、名字发现/服务发现

名字发现和服务发现分为两种模式,一个是客户端发现模式,一种是服务端发现模式。

框架中常用的服务发现是客户端发现模式。

所谓服务端发现模式是指客户端通过一个负载均衡器向服务发送请求,负载均衡器查询服务注册表并把请求路由到一台可用的服务实例上。现在常用的负载均衡器都是此类模式,常用于微服务中。

所有的名字发现和服务发现都要依赖于一个可用性非常高的服务注册表,业界常用的服务注册表有如下三个:

  • etcd,一个高可用、分布式、一致性、key-value 方式的存储,被用在分享配置和服务发现中。两个著名的项目使用了它:Kubernetes 和 Cloud Foundry。
  • Consul,一个发现和配置服务的工具,为客户端注册和发现服务提供了API,Consul还可以通过执行健康检查决定服务的可用性。
  • Apache ZooKeeper,是一个广泛使用、高性能的针对分布式应用的协调服务。Apache ZooKeeper 本来是 Hadoop 的子工程,现在已经是顶级工程了。

除此之外也可以自己实现服务实现,或者用 Redis 也行,只是需要自己实现高可用性。

7、关系数据库

关系数据库分为两种,一种是传统关系数据,如 Oracle,MySQL,Maria,DB2,PostgreSQL 等等,另一种是 NewSQL,即至少要满足以下五点的新型关系数据库:

  1. 完整地支持 SQL,支持 JOIN / GROUP BY /子查询等复杂 SQL 查询。
  2. 支持传统数据标配的 ACID 事务,支持强隔离级别。
  3. 具有弹性伸缩的能力,扩容缩容对于业务层完全透明。
  4. 真正的高可用,异地多活、故障恢复的过程不需要人为的接入,系统能够自动地容灾和进行强一致的数据恢复。
  5. 具备一定的大数据分析能力。

传统关系数据库用得最多的是 MySQL,成熟,稳定,一些基本的需求都能满足,在一定数据量级之前基本单机传统数据库都可以搞定,而且现在较多的开源系统都是基于 MySQL,开箱即用,再加上主从同步和前端缓存,百万 pv 的应用都可以搞定了。不过 CentOS 7 已经放弃了 MySQL,而改使用 MariaDB。MariaDB 数据库管理系统是 MySQ L的一个分支,主要由开源社区在维护,采用 GPL 授权许可。开发这个分支的原因之一是:甲骨文公司收购了 MySQL 后,有将 MySQL 闭源的潜在风险,因此社区采用分支的方式来避开这个风险。

在 Google 发布了 F1: A Distributed SQL Database That Scales 和 Spanner: Google’s Globally-Distributed Databasa 之后,业界开始流行起 NewSQL。于是有了 CockroachDB,于是有了奇叔公司的 TiDB。国内已经有比较多的公司使用 TiDB,之前在创业公司时在大数据分析时已经开始应用 TiDB,当时应用的主要原因是 MySQL 要使用分库分表,逻辑开发比较复杂,扩展性不够。

8、NoSQL

NoSQL 顾名思义就是 Not-Only SQL,也有人说是 No – SQL,个人偏向于 Not-Only SQL,它并不是用来替代关系库,而是作为关系型数据库的补充而存在。

常见 NoSQL 有4个类型:

  • 键值,适用于内容缓存,适合混合工作负载并发高扩展要求大的数据集,其优点是简单,查询速度快,缺点是缺少结构化数据,常见的有 Redis,Memcache,BerkeleyDB 和 Voldemort 等等;
  • 列式,以列簇式存储,将同一列数据存在一起,常见于分布式的文件系统,其中以 Hbase,Cassandra 为代表。Cassandra 多用于写多读少的场景,国内用得比较多的有 360,大概 1500 台机器的集群,国外大规模使用的公司比较多,如 eBay,Instagram,Apple 和沃尔玛等等;
  • 文档,数据存储方案非常适用承载大量不相关且结构差别很大的复杂信息。性能介于 kv 和关系数据库之间,它的灵感来于 lotus notes,常见的有 MongoDB,CouchDB 等等;
  • 图形,图形数据库擅长处理任何涉及关系的状况。社交网络,推荐系统等。专注于构建关系图谱,需要对整个图做计算才能得出结果,不容易做分布式的集群方案,常见的有 Neo4J,InfoGrid 等。

除了以上4种类型,还有一些特种的数据库,如对象数据库,XML 数据库,这些都有针对性对某些存储类型做了优化的数据库。

在实际应用场景中,何时使用关系数据库,何时使用 NoSQL,使用哪种类型的数据库,这是我们在做架构选型时一个非常重要的考量,甚至会影响整个架构的方案。

9、消息中间件

消息中间件在后台系统中是必不可少的一个组件,一般我们会在以下场景中使用消息中间件:

  • 异步处理:异步处理是使用消息中间件的一个主要原因,在工作中最常见的异步场景有用户注册成功后需要发送注册成功邮件、缓存过期时先返回老的数据,然后异步更新缓存、异步写日志等等;通过异步处理,可以减少主流程的等待响应时间,让非主流程或者非重要业务通过消息中间件做集中的异步处理。
  • 系统解耦:比如在电商系统中,当用户成功支付完成订单后,需要将支付结果给通知ERP系统、发票系统、WMS、推荐系统、搜索系统、风控系统等进行业务处理;这些业务处理不需要实时处理、不需要强一致,只需要最终一致性即可,因此可以通过消息中间件进行系统解耦。通过这种系统解耦还可以应对未来不明确的系统需求。
  • 削峰填谷:当系统遇到大流量时,监控图上会看到一个一个的山峰样的流量图,通过使用消息中间件将大流量的请求放入队列,通过消费者程序将队列中的处理请求慢慢消化,达到消峰填谷的效果。最典型的场景是秒杀系统,在电商的秒杀系统中下单服务往往会是系统的瓶颈,因为下单需要对库存等做数据库操作,需要保证强一致性,此时使用消息中间件进行下单排队和流控,让下单服务慢慢把队列中的单处理完,保护下单服务,以达到削峰填谷的作用。

业界消息中间件是一个非常通用的东西,大家在做选型时有使用开源的,也有自己造轮子的,甚至有直接用 MySQL 或 Redis 做队列的,关键看是否满足你的需求,如果是使用开源的项目,以下的表格在选型时可以参考:

图片

图 3

以上图的纬度为:名字、成熟度、所属社区/公司、文档、授权方式、开发语言、支持的协议、客户端支持的语言、性能、持久化、事务、集群、负载均衡、管理界面、部署方式、评价。

10、代码管理

代码是互联网创业公司的命脉之一,代码管理很重要,常见的考量点包括两块:

  • 安全和权限管理,将代码放到内网并且对于关系公司命脉的核心代码做严格的代码控制和机器的物理隔离;
  • 代码管理工具,Git 作为代码管理的不二之选,你值得拥有。GitLab 是当今最火的开源 Git 托管服务端,没有之一,虽然有企业版,但是其社区版基本能满足我们大部分需求,结合 Gerrit 做 Code review,基本就完美了。当然 GitLab 也有代码对比,但没 Gerrit 直观。Gerrit 比 GitLab 提供了更好的代码检查界面与主线管理体验,更适合在对代码质量有高要求的文化下使用。
11、持续集成

持续集成简,称 CI(continuous integration),是一种软件开发实践,即团队开发成员经常集成他们的工作,每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。持续集成为研发流程提供了代码分支管理/比对、编译、检查、发布物输出等基础工作,为测试的覆盖率版本编译、生成等提供统一支持。

业界免费的持续集成工具中系统我们有如下一些选择:

  • Jenkins:Java 写的有强大的插件机制,MIT 协议开源 (免费,定制化程度高,它可以在多台机器上进行分布式地构建和负载测试)。Jenkins 可以算是无所不能,基本没有 Jenkins 做不了的,无论从小型团队到大型团队 Jenkins 都可以搞定。不过如果要大规模使用,还是需要有人力来学习和维护。
  • TeamCity:TeamCity 与 Jenkins 相比使用更加友好,也是一个高度可定制化的平台。但是用的人多了,TeamCity就要收费了。
  • Strider:Strider 是一个开源的持续集成和部署平台,使用 Node.js 实现,存储使用的是 MongoDB,BSD 许可证,概念上类似 Travis 和Jenkins。
  • GitLab CI:从GitLab 8.0开始,GitLab CI 就已经集成在 GitLab,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成。并且 GitLab 与 Docker 有着非常好的相互协作的能力。免费版与付费版本不同可以参见这里:https://about.gitlab.com/products/feature-comparison/。
  • Travis:Travis 和 GitHub 强关联;闭源代码使用 SaaS 还需考虑安全问题;不可定制;开源项目免费,其它收费。
  • Go:Go 是 ThoughtWorks 公司最新的 Cruise Control 的化身。除了 ThoughtWorks 提供的商业支持,Go 是免费的。它适用于 Windows,Mac 和各种 Linux 发行版。
12、日志系统

日志系统一般包括打日志,采集,中转,收集,存储,分析,呈现,搜索还有分发等。一些特殊的如染色,全链条跟踪或者监控都可能需要依赖于日志系统实现。日志系统的建设不仅仅是工具的建设,还有规范和组件的建设,最好一些基本的日志在框架和组件层面加就行了,比如全链接跟踪之类的。

对于常规日志系统ELK能满足大部分的需求,ELK 包括如下组件:

  • ElasticSearch 是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,RESTful 风格接口,多数据源,自动搜索负载等。
  • Logstash 是一个完全开源的工具,它可以对你的日志进行收集、分析,并将其存储供以后使用。
  • Kibana 是一个开源和免费的工具,它可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志。
  • Filebeat 已经完全替代了 Logstash-Forwarder 成为新一代的日志采集器,同时鉴于它轻量、安全等特点,越来越多人开始使用它。

因为免费的 ELK 没有任何安全机制,所以这里使用了 Nginx 作反向代理,避免用户直接访问 Kibana 服务器。加上配置 Nginx 实现简单的用户认证,一定程度上提高安全性。另外,Nginx 本身具有负载均衡的作用,能够提高系统访问性能。ELK 架构如图4所示:

图片

图 4,ELK 流程图

对于有实时计算的需求,可以使用 Flume + Kafka + Storm + MySQL 方案,一 般架构如图 5 所示:

图片

图 5,实时分析系统架构图

其中:

  • Flume 是一个分布式、可靠、和高可用的海量日志采集、聚合和传输的日志收集系统,支持在日志系统中定制各类数据发送方,用于收集数据;同时,Flume 提供对数据进行简单处理,并写到各种数据接受方(可定制)的能力。
  • Kafka 是由 Apache 软件基金会开发的一个开源流处理平台,由 Scala 和 Java 编写。其本质上是一个“按照分布式事务日志架构的大规模发布/订阅消息队列”,它以可水平扩展和高吞吐率而被广泛使用。

Kafka 追求的是高吞吐量、高负载,Flume 追求的是数据的多样性,二者结合起来简直完美。

13、监控系统

监控系统只包含与后台相关的,这里主要是两块,一个是操作系统层的监控,比如机器负载,IO,网络流量,CPU,内存等操作系统指标的监控。另一个是服务质量和业务质量的监控,比如服务的可用性,成功率,失败率,容量,QPS 等等。常见业务的监控系统先有操作系统层面的监控(这部分较成熟),然后扩展出其它监控,如 Zabbix,小米的 Open-Falcon,也有一出来就是两者都支持的,如 Prometheus。如果对业务监控要求比较高一些,在创业选型中建议可以优先考虑 Prometheus。这里有一个有趣的分布,如图6所示。

图片

图 6,监控系统分布

亚洲区域使用 Zabbix 较多,而美洲和欧洲,以及澳大利亚使用 Prometheus 居多,换句话说,英文国家地区(发达国家?)使用 Prometheus 较多。

Prometheus 是由 SoundCloud 开发的开源监控报警系统和时序列数据库(TSDB)。Prometheus 使用 Go 语言开发,是 Google BorgMon 监控系统的开源版本。相对于其它监控系统使用的 push 数据的方式,Prometheus 使用的是 pull 的方式,其架构如图 7 所示:

图片

图 7,Prometheus 架构图

如上图所示,Prometheus 包含的主要组件如下:

  • Prometheus Server 主要负责数据采集和存储,提供 PromQL 查询语言的支持。Server 通过配置文件、文本文件、ZooKeeper、Consul、DNS SRV Lookup 等方式指定抓取目标。根据这些目标会,Server 定时去抓取 metrics 数据,每个抓取目标需要暴露一个 http 服务的接口给它定时抓取。
  • 客户端 SDK:官方提供的客户端类库有 Go、Java、Scala、Python、Ruby,其他还有很多第三方开发的类库,支持 Nodejs、PHP、Erlang 等。
  • Push Gateway 支持临时性 Job 主动推送指标的中间网关。
  • Exporter Exporter 是 Prometheus 的一类数据采集组件的总称。它负责从目标处搜集数据,并将其转化为 Prometheus 支持的格式。与传统的数据采集组件不同的是,它并不向中央服务器发送数据,而是等待中央服务器主动前来抓取。Prometheus 提供多种类型的 Exporter 用于采集各种不同服务的运行状态。目前支持的有数据库、硬件、消息中间件、存储系统、HTTP 服务器、JMX 等。
  • Alertmanager:是一个单独的服务,可以支持 Prometheus 的查询语句,提供十分灵活的报警方式。
  • Prometheus HTTP API 的查询方式,自定义所需要的输出。
  • Grafana 是一套开源的分析监视平台,支持 Graphite,InfluxDB,OpenTSDB,Prometheus,Elasticsearch,CloudWatch 等数据源,其 UI 非常漂亮且高度定制化。

创业公司选择 Prometheus + Grafana 的方案,再加上统一的服务框架(如 gRPC),可以满足大部分中小团队的监控需求。

14、配置系统

随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、降级开关,灰度开关,参数的配置、服务器的地址、数据库配置等等,除此之外,对后台程序配置的要求也越来越高:配置修改后实时生效,灰度发布,分环境、分用户,分集群管理配置,完善的权限、审核机制等等,在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求,业界有如下两种方案:

  • 基于 zk 和 etcd,支持界面和 api ,用数据库来保存版本历史,预案,走审核流程,最后下发到 zk 或 etcd 这种有推送能力的存储里(服务注册本身也是用 zk 或 etcd,选型就一块了)。客户端都直接和 zk 或 etcd 打交道。至于灰度发布,各家不同,有一种实现是同时发布一个需要灰度的 IP 列表,客户端监听到配置节点变化时,对比一下自己是否属于该列表。PHP 这种无状态的语言和其他 zk/etcd 不支持的语言,只好自己在客户端的机器上起一个 Agent 来监听变化,再写到配置文件或共享内存,如 360 的 Qconf。
  • 基于运维自动化的配置文件的推送,审核流程,配置数据管理和方案一类似,下发时生成配置文件,基于运维自动化工具如 Puppet,Ansible 推送到每个客户端,而应用则定时重新读取这个外部的配置文件,灰度发布在下发配置时指定IP列表。

创业公司前期不需要这种复杂,直接上 zk,弄一个界面管理 zk 的内容,记录一下所有人的操作日志,程序直连 zk,或者或者用 Qconf 等基于 zk 优化后的方案。

15、发布系统/部署系统

从软件生产的层面看,代码到最终服务的典型流程如图 8 所示:

图片

图 8,流程图

从上图中可以看出,从开发人员写下代码到服务最终用户是一个漫长过程,整体可以分成三个阶段:

  • 从代码(Code)到成品库(Artifact)这个阶段主要对开发人员的代码做持续构建并把构建产生的制品集中管理,是为部署系统准备输入内容的阶段。
  • 从制品到可运行服务 这个阶段主要完成制品部署到指定环境,是部署系统的最基本工作内容。
  • 从开发环境到最终生产环境 这个阶段主要完成一次变更在不同环境的迁移,是部署系统上线最终服务的核心能力。

发布系统集成了制品管理,发布流程,权限控制,线上环境版本变更,灰度发布,线上服务回滚等几方面的内容,是开发人员工作结晶最终呈现的重要通道。开源的项目中没有完全满足的项目,如果只是 Web 类项目,Walle、Piplin 都是可用的,但是功能不太满足,创业初期可以集成 Jenkins + Gitlab + Walle(可以考虑两天时间完善一下),以上方案基本包括制品管理,发布流程,权限控制,线上环境版本变更,灰度发布(需要自己实现),线上服务回滚等功能。

16、跳板机

跳板机面对的是需求是要有一种能满足角色管理与授权审批、信息资源访问控制、操作记录和审计、系统变更和维护控制要求,并生成一些统计报表配合管理规范来不断提升IT内控的合规性,能对运维人员操作行为的进行控制和审计,对误操作、违规操作导致的操作事故,快速定位原因和责任人。其功能模块一般包括:帐户管理、认证管理、授权管理、审计管理等等。

开源项目中,Jumpserver 能够实现跳板机常见需求,如授权、用户管理、服务器基本信息记录等,同时又可批量执行脚本等功能;其中录像回放、命令搜索、实时监控等特点,又能帮助运维人员回溯操作历史,方便查找操作痕迹,便于管理其他人员对服务器的操作控制。

17、机器管理

机器管理的工具选择的考量可以包含以下三个方面:

  1. 是否简单,是否需要每台机器部署 Agent(客户端)
  2. 语言的选择(Puppet/Chef vs Ansible/SaltStack )开源技术,不看官网不足以熟练,不懂源码不足以精通;Puppet、Chef 基于 Ruby 开发,Ansible、SaltStack 基于 Python 开发的
  3. 速度的选择(Ansible vs SaltStack)Ansible 基于 SSH 协议传输数据,SaltStack 使用消息队列 zeroMQ 传输数据;大规模并发的能力对于几十台-200 台规模的兄弟来讲,Ansible的性能也可接受,如果一次操作上千台,用 salt 好一些。

如图9所示:

图片

图 9,机器管理软件对比

一般创业公司选择 Ansible 能解决大部问题,其简单,不需要安装额外的客户端,可以从命令行来运行,不需要使用配置文件。至于比较复杂的任务,Ansible 配置通过名为 Playbook 的配置文件中的 YAML 语法来加以处理。Playbook 还可以使用模板来扩展其功能。

创业公司的选择

1、选择合适的语言

  • 选择团队熟悉的/能掌控的,创业公司人少事多,无太多冗余让研发团队熟悉新的语言,能快速上手,能快速出活,出了问题能快速解决的问题的语言才是好的选择。
  • 选择更现代一些的,这里的现代是指语言本身已经完成一些之前需要特殊处理的特性,比如内存管理,线程等等。
  • 选择开源轮子多的或者社区活跃度高的,这个原则是为了保证在开发过程中减少投入,有稳定可靠的轮子可以使用,遇到问题可以在网上快速搜索到答案。
  • 选择好招人的 一门合适的语言会让创业团队减少招聘的成本,快速招到合适的人。
  • 选择能让人有兴趣的 与上面一点相关,让人感兴趣,在后面留人时有用。
2、选择合适的组件和云服务商

  • 选择靠谱的云服务商;
  • 选择云服务商的组件;
  • 选择成熟的开源组件,而不是最新出的组件;
  • 选择采用在一线互联网公司落地并且开源的,且在社区内形成良好口碑的产品;
  • 开源社区活跃度;

选择靠谱的云服务商,其实这是一个伪命题,因为哪个服务商都不靠谱,他们所承诺的那些可用性问题基本上都会在你的身上发生,这里我们还是需要自己做一些工作,比如多服务商备份,如用 CDN,你一定不要只选一家,至少选两家,一个是灾备,保持后台切换的能力,另一个是多点覆盖,不同的服务商在 CDN 节点上的资源是不一样的。

选择了云服务商以后,就会有很多的产品你可以选择了,比较存储,队列这些都会有现成的产品,这个时候就纠结了,是用呢?还是自己在云主机上搭呢?在这里我的建议是前期先用云服务商的,大了后再自己搞,这样会少掉很多运维的事情,但是这里要多了解一下云服务商的组件特性以及一些坑,比如他们内网会经常断开,他们升级也会闪断,所以在业务侧要做好容错和规避。

关于开源组件,尽可能选择成熟的,成熟的组件经历了时间的考验,基本不会出大的问题,并且有成套的配套工具,出了问题在网上也可以很快的找到答案,你所遇到的坑基本上都有人踩过了。

3、制定流程和规范

  • 制定开发的规范,代码及代码分支管理规范,关键性代码仅少数人有权限;
  • 制定发布流程规范,从发布系统落地;
  • 制定运维规范;
  • 制定数据库操作规范,收拢数据库操作权限;
  • 制定告警处理流程,做到告警有人看有人处理;
  • 制定汇报机制,晨会/周报;
4、自研和选型合适的辅助系统

所有的流程和规范都需要用系统来固化,否则就是空中楼阁,如何选择这些系统呢?参照上个章节咱们那些开源的,对比一下选择的语言,组件之类的,选择一个最合适的即可。

比如项目管理的,看下自己是什么类型的公司,开发的节奏是怎样的,瀑布,敏捷的 按项目划分,还是按客户划分等等,平时是按项目组织还是按任务组织等等。

比如日志系统,之前是打的文本,那么上一个 ELK,规范化一些日志组件,基本上很长一段时间内不用考虑日志系统的问题,最多拆分一下或者扩容一下。等到组织大了,自己搞一个日志系统。

比如代码管理,项目管理系统这些都放内网,安全,在互联网公司来说,属于命脉了,命脉的东西还是放在别人拿不到或很难拿到的地方会比较靠谱一些。

5、选择过程中需要思考的问题

技术栈的选择有点像做出了某种承诺,在一定的时间内这种承诺没法改变,于是我们需要在选择的时候有一些思考。

看前面内容,有一个词出现了三次,合适,选择是合适的,不是最好,也不是最新,是最合适,适合是针对当下,这种选择是最合适的吗?比如用 Go 这条线的东西,技术比较新,业界组件储备够吗?组织内的人员储备够吗?学习成本多少?写出来的东西能满足业务性能要求吗?能满足时间要求吗?

向未来看一眼,在一年到三年内,我们需要做出改变吗?技术栈要做根本性的改变吗?如果组织发展很快,在 200 人,500 人时,现有的技术栈是否需要大动?

创业过程中需要考虑成本,这里的成本不仅仅是花费多少钱,付出多少工资,有时更重要的是时间成本,很多业务在创业时大家拼的就是时间,就是一个时间窗,过了就没你什么事儿了。

基于云的创业公司后台技术架构

结合上面内容的考量,在对一个个系统和组件的做选型之后,以云服务为基础,一个创业公司的后台技术架构如图10所示:

图片

图 10,后台技术架构

参考资料

http://database.51cto.com/art/201109/291781.htm

https://zh.wikipedia.org/wiki/Kafka

https://prometheus.io/docs/introduction/overview/

http://deadline.top/2016/11/23/配置中心那点事/

http://blog.fit2cloud.com/2016/01/26/deployment-system.html

转自 : http://ju.outofmemory.cn/entry/351897

编辑:公众号程序员面试

sqlserver2008r2 服务自动停止_Windows Server 2008 R2服务器内存使用率过高,但与任务管理器中进程占用内存和不一致..._weixin_39917894的博客-CSDN博客

mikel阅读(780)

来源: sqlserver2008r2 服务自动停止_Windows Server 2008 R2服务器内存使用率过高,但与任务管理器中进程占用内存和不一致…_weixin_39917894的博客-CSDN博客

系统环境:
Windows Server 2008 R2 + SQL Server 2008 R2

问题描述:
Windows Server 2008 R2系统内存占用率过大,而在任务管理器中各进程内存占用总和都远不到此占用率。
相关现象:

内存占用率90%以上

任务管理器中所有进程内存相加较低,远不到90%,有二十多G的内存偏差

分析过程:

首先怀疑SQL Server内存占用,但是SQL Server设置了最大内存,且任务管理器中显示的内存占比并没有达到很高,排除此原因
用RamMap工具查看内存的详细使用情况,发现图元文件(Metafile)占用了二十多G的内存,应该与此有关

解决方法:利用RamMap的“清空系统工作集”功能,可以释放内存。

图元文件(Metafile):
Metafile可以理解为系统缓存,Windows server 2008系统中,比如存在大量的文件拷贝等磁盘io操作,系统会自动将其缓存到内存中,这部分被占用的内存在任务管理器的中未体现出来的,所以用户会认为系统的内存占用异常。同时,MetaFile默认是没有限制的,所以系统会无限制占用内存。但是对于Windows 2012之后,操作系统会自动限制系统缓存的上限从而避免物理内存的耗尽。
看了图元文件的解释后,发现前两天一直在做转移文件的操作,几百G的大量小文件在不同磁盘中转移,从而导致内存占用过高,符合此解释。至此,问题调查清楚。

如下, 是微软官方Blog中介绍修改注册表来限制动态缓存的方法:
Microsoft Windows Dynamic Cache的更新程序,用户可以下载后添加到服务中,通过修改注册表来限制动态缓存的最大值,比如设置上限为200M等,这样系统就可以把作为缓存使用的内存设置了上限值。
下载地址: http://www.microsoft.com/en-us/download/details.aspx?id=9258
具体设置步骤如下:

解压后根据不同系统版本中对应的DynCache.exe文件复制到C:\windows\system32中。

以管理员身份打开命令提示符,执行如下命令添加服务:
sc create DynCache binPath= %SystemRoot%\System32\DynCache.exe start= auto type= own DisplayName= “Dynamic Cache Service”

回到DynCache文件夹,找到DynCache.reg的注册表文件导入。

打开注册表,找到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DynCache\Parameters

右侧找到MaxSystemCacheMBytes,双击它,这里我们选择“十进制”,在数值里输入要限制最大的缓存数(单位是MB),输入800就是限制缓存最大为800MB,输入0为不限制。

到服务中启动DynCache服务。
注:下载的dyncache解压出来是有多几个版本可选的,请选择retail amd64的版本,不要选择ia64(ia64表示安腾的64位处理器版本)。

转载于:https://www.cnblogs.com/Juning/p/11076632.html
————————————————
版权声明:本文为CSDN博主「weixin_39917894」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39917894/article/details/111331467

MQ 入门实践 - 曹建涛 - 博客园

mikel阅读(1073)

来源: MQ 入门实践 – 曹建涛 – 博客园

MQ

Message Queue,消息队列,FIFO 结构。

例如电商平台,在用户支付订单后执行对应的操作;

优点:

  • 异步
  • 削峰
  • 解耦

缺点

  • 增加系统复杂性
  • 数据一致性
  • 可用性

JMS

Java Message Service,Java消息服务,类似 JDBC 提供了访问数据库的标准,JMS 也制定了一套系统间消息通信的规范;

区别于 JDBC,JDK 原生包中并未定义 JMS 相关接口。

  1. ConnectionFactory
  2. Connection
  3. Destination
  4. Session
  5. MessageConsumer
  6. MessageProducer
  7. Message

协作方式图示为;

业界产品

ActiveMQ RabbitMQ RocketMQ kafka
单机吞吐量 万级 万级 10 万级 10 万级
可用性 非常高 非常高
可靠性 较低概率丢失消息 基本不丢 可以做到 0 丢失 可以做到 0 丢失
功能支持 较为完善 基于 erlang,并发强,性能好,延时低 分布式,拓展性好,支持分布式事务 较为简单,主要应用与大数据实时计算,日志采集等
社区活跃度

ActiveMQ

作为 Apache 下的开源项目,完全支持 JMS 规范。并且 Spring Boot 内置了 ActiveMQ 的自动化配置,作为入门再适合不过。

快速开始

添加依赖;

<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-core</artifactId>
    <version>5.7.0</version>
</dependency>

消息发送;

// 1. 创建连接工厂
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
// 2. 工厂创建连接
Connection connection = factory.createConnection();
// 3. 启动连接
connection.start();
// 4. 创建连接会话session,第一个参数为是否在事务中处理,第二个参数为应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 5. 根据session创建消息队列目的地
Destination queue = session.createQueue("test-queue");
// 6. 根据session和目的地queue创建生产者
MessageProducer producer = session.createProducer(queue);
// 7. 根据session创建消息实体
Message message = session.createTextMessage("hello world!");
// 8. 通过生产者producer发送消息实体
producer.send(message);
// 9. 关闭连接
connection.close();

Spring Boot 集成

自动注入参考:org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionFactoryConfiguration.SimpleConnectionFactoryConfiguration

添加依赖;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

添加 yaml 配置;

spring:
  activemq:
    broker-url: tcp://localhost:61616
  jms:
    #消息模式 true:广播(Topic),false:队列(Queue),默认时false
    pub-sub-domain: true

收发消息;

@Autowired
private JmsTemplate jmsTemplate;

// 接收消息
@JmsListener(destination = "test")
public void receiveMsg(String msg) {
    System.out.println(msg);
}

// 发送消息
public void sendMsg(String destination, String msg) {
    jmsTemplate.convertAndSend(destination, msg);
}

高可用

基于 zookeeper 实现主从架构,修改 activemq.xml 节点 persistenceAdapter 配置;

<persistenceAdapter>
    <replicatedLevelDB
        directory="${activemq.data}/levelDB"
        replicas="3"
        bind="tcp://0.0.0.0:0"
        zkAddress="172.17.0.4:2181,172.17.0.4:2182,172.17.0.4:2183"
        zkPath="/activemq/leveldb-stores"
        hostname="localhost"
    />
</persistenceAdapter>

broker 地址为:failover:(tcp://192.168.4.19:61616,tcp://192.168.4.19:61617,tcp://192.168.4.19:61618)?randomize=false

负载均衡

在高可用集群节点 activemq.xml 添加节点 networkConnectors;

<networkConnectors>
    <networkConnector uri="static:(tcp://192.168.0.103:61616,tcp://192.168.0.103:61617,tcp://192.168.0.103:61618)" duplex="false"/>
</networkConnectors>

更多详细信息可参考:https://blog.csdn.net/haoyuyang/article/details/53931710

集群消费

由于发布订阅模式,所有订阅者都会接收到消息,在生产环境,消费者集群会产生消息重复消费问题。

ActiveMQ 提供 VirtualTopic 功能,解决多消费端接收同一条消息的问题。于生产者而言,VirtualTopic 就是一个 topic,对消费而言则是 queue。

在 activemq.xml 添加节点 destinationInterceptors;

<destinationInterceptors> 
    <virtualDestinationInterceptor> 
        <virtualDestinations> 
            <virtualTopic name="testTopic" prefix="consumer.*." selectorAware="false"/>    
        </virtualDestinations>
    </virtualDestinationInterceptor> 
</destinationInterceptors>

生产者正常往 testTopic 中发送消息,订阅者可修改订阅主题为类似 consumer.A.testTopic 这样来消费。

更多详细信息可参考:https://blog.csdn.net/java_collect/article/details/82154829

RocketMQ

是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。

架构图示

  1. Name Server

    名称服务器,类似于 Zookeeper 注册中心,提供 Broker 发现;

  2. Broker

    RocketMQ 的核心组件,绝大部分工作都在 Broker 中完成,接收请求,处理消费,消息持久化等;

  3. Producer

    消息生产方;

  4. Consumer

    消息消费方;

快速开始

安装后,依次启动 nameserver 和 broker,可以用 mqadmin 管理主题、集群和 broker 等信息;

https://segmentfault.com/a/1190000017841402

添加依赖;

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.5.2</version>
</dependency>

消息发送;

DefaultMQProducer producer = new DefaultMQProducer("producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.setInstanceName("producer");
producer.start();
Message msg = new Message(
    "producer-topic",
    "msg",
    "hello world".getBytes()
);
//msg.setDelayTimeLevel(1);
SendResult sendResult = producer.send(msg);
System.out.println(sendResult.toString());
producer.shutdown();

delayLevel 从 1 开始默认依次是:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。

参考 org.apache.rocketmq.store.schedule.ScheduleMessageService#parseDelayLevel。

消息接收;

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.setInstanceName("consumer");
consumer.subscribe("producer-topic", "msg");
consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
    for (MessageExt msg : list) {
        System.out.println(new String(msg.getBody()));
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();

.\mqadmin.cmd sendMessage -t producer-topic -c msg -p “hello rocketmq” -n localhost:9876

Spring Boot 集成

添加依赖;

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.4</version>
</dependency>

添加 yaml 配置;

rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: producer

发送消息;

@Autowired
private RocketMQTemplate mqTemplate;

public void sendMessage(String topic, String tag, String message) {
    SendResult result = mqTemplate.syncSend(topic + ":" + tag, message);
    System.out.println(JSON.toJSONString(result));
}

接收消息;

@Component
@RocketMQMessageListener(consumerGroup = "consumer", topic = "topic-test", selectorExpression = "tag-test")
public class MsgListener implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

Console 控制台

RocketMQ 拓展包提供了管理控制台;

https://github.com/apache/rocketmq-externals/tree/master/rocketmq-console

重复消费

产生原因:

  1. 生产者重复投递;
  2. 消息队列异常;
  3. 消费者异常消费;

怎么解决重复消费的问题,换句话怎么保证消息消费的幂等性

通常基于本地消息表的方案实现,消息处理过便不再处理。

顺序消息

消息错乱的原因:

  1. 一个消息队列 queue,多个 consumer 消费;
  2. 一个 queue 对应一个 consumer,但是 consumer 多线程消费;

要保证消息的顺序消费,有三个关键点:

  1. 消息顺序发送
  2. 消息顺序存储
  3. 消息顺序消费

参考 RocketMq 中的 MessageQueueSelector 和 MessageListenerOrderly。

分布式事务

在分布式系统中,一个事务由多个本地事务组成。这里介绍一个基于 MQ 的分布式事务解决方案。

通过 broker 的 HA 高可用,和定时回查 prepare 消息的状态,来保证最终一致性。

.net core 插件式开发 - FreeTimeWorker - 博客园

mikel阅读(763)

来源: .net core 插件式开发 – FreeTimeWorker – 博客园

插件式开发

思考一种情况,短信发送,默认实现中只写了一种实现,因为某些原因该模块的所依赖的第三方无法继续提供服务,或者对于winform程序,某按钮单击,需要在运行时增加额外的操作,或者替换目前使用的功能,对于类似这样的需求,可以考虑使用插件式的方式搭建框架,以实现更灵活的可拆卸动态增加功能。 .net core 中提供了一种热加载外部dll的方式,可以满足该类型的需求 AssemblyLoadContext

流程

1,定义针对系统中所有可插拔点的接口
2,针对接口开发插件/增加默认实现
3,根据需要,在运行时执行相应的逻辑
4,在动态载入dll时谨防内存泄漏

代码

1,定义接口

在单独的类库中定义针对插拔点的接口

    public interface ICommand
    {
        string Name { get; }
        string Description { get; }
        int Execute();
    }

2,开发插件

新建类库,引用接口所在的类库,值得注意的的是 CopyLocalLockFileAssemblies,表示将所有依赖项生成到生成目录,对于插件中有对其他项目或者类库有引用的这个属性是必须的,Private表示引用的类库为公共程序集,该属性默认为true,为使插件可以正确在运行时加载,该属性必须为 ** false **

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>net5.0</TargetFramework>
		<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
	</PropertyGroup>
	<ItemGroup>
	  <PackageReference Include="AutoMapper" Version="10.1.1" />
	  <PackageReference Include="System.Text.Json" Version="4.6.0" />
	</ItemGroup>
	<ItemGroup>
	  <ProjectReference Include="..\Plugins\Plugins.csproj">
		  <Private>false</Private>
		  <ExcludeAssets>runtime</ExcludeAssets>
		</ProjectReference>
	</ItemGroup>
</Project>

修改完类库中这两处的值以后添加类,继承自ICommand 将接口定义的方法和属性做相关的实现,如下

    public class Class1 : ICommand
    {
        public string Name => "Classb";
        public string Description => "Classb Description";
        public int Execute()
        {
            var thisv = JsonSerializer.Serialize(this);
            Assembly ass = typeof(AutoMapper.AdvancedConfiguration).Assembly;
            Console.WriteLine(ass.FullName);
            Console.WriteLine(thisv);
            Console.WriteLine("111111111111111111111111111111111111111111");
            return 10000;
        }
    }

3,根据需要在运行时执行相应逻辑

编写用于运行时 插件加载上下文, 该类主要负责将给定路径的dll加载到当前应用程序域,静态方法用户获取实现了插件接口的实例

  public class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;
        public PluginLoadContext(string pluginPath,bool isCollectible) :base(isCollectible)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }
        //加载依赖项
        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }
            return null;
        }
        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }
            return IntPtr.Zero;
        }
  
        public static List<ICommand> CreateCommands(string[] pluginPaths)
        {
            List<Assembly> _assemblies = new List<Assembly>();
            foreach (var pluginPath in pluginPaths)
            {
                string pluginLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, pluginPath.Replace('\\', Path.DirectorySeparatorChar)));
                var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(o => o.Location == pluginLocation);
                //根据程序集的物理位置判断当前域中是否存在该类库,如果不存在就读取,如果存在就从当前程序域中读取,由于AssemblyLoadContext已经做了相应的上下文隔离
                //,所以即便是名称一样位置一样也可以重复加载,执行也可以按照预期执行,但由于会重复加载程序集,就会造成内存一直增加导致内存泄漏
                if (assembly == null)
                {
                    PluginLoadContext pluginLoadContext = new PluginLoadContext(pluginLocation, true);
                    assembly = pluginLoadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
                }
                _assemblies.Add(assembly);
            }
            var results = new List<ICommand>();
            foreach (var assembly in _assemblies)
            {
                foreach (Type type in assembly.GetTypes())
                {
                    if (typeof(ICommand).IsAssignableFrom(type))
                    {
                        ICommand result = Activator.CreateInstance(type) as ICommand;
                        if (result != null)
                        {
                            results.Add(result);
                        }
                    }
                }
            }
            return results;
        }
    }

调用

            try
            {
                //插件添加后,相应的位置保存下载
                string[] pluginPaths = new string[]
                {
                    "Plugin/PluginA/PluginA.dll",//将插件所在类库生成后的文件复制到PluginA下边
                };
                var i = 0;
                while (true)
                {
                    List<ICommand> commands = PluginLoadContext.CreateCommands(pluginPaths);
                    foreach (var command in commands)
                    {
                        Console.WriteLine(command.Name);
                        Console.WriteLine(command.Description);
                        Console.WriteLine(command.Execute());
                    }
                }
                
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            Console.ReadKey();

图2中去掉了当前程序集中根据地址确定是否重新加载插件,可以看到内存的使用量在一直增加,最终一定会导致溢出。

对比图 1

对比图 2

对于插件卸载,我认为没有必要去考虑,对于同一类型插件,只需要将不同版本的放到不同的位置,在一个公共位置维护当前使用的插件所在位置,如果有更新直接找最新的实现去执行就行,卸载很麻烦,需要删除掉所有的依赖项,还容易出错,不解决就是最好的解决方案

.NetCore 与 .NetFramework 动态加载DLL的一些不同点

mikel阅读(810)

来源: .NetCore 与 .NetFramework 动态加载DLL的一些不同点

在.NetCore实现插件系统的时候,发现某些方法与在.NetFramework内的使用方法不一样,在此做一个记录供以后查阅。

NO.1 Assembly.Load 与 AssemblyLoadContext.Default.LoadFromXXX

在.NetFramework当中,如果要加载某一个程序集的话,通过Assembly的Load的各种重载方法即可通过文件流、文件路
径等方式将其加载到当前调用方的应用程序域当中。但是在.Net Core当中则不然,它并没有在Assembly当中提供Load
方法供开发人员操作,而你则需要在Nuget里面对当前项目安装System.Runtime.Loader,然后使用其提供的
AssemblyLoadContext来进行程序集加载。

引用自OSChina的一篇资讯

App Domain

App Domain在CoreCLR中得以实现,但没有在.NET Native中实现。由于对App Domain的实现需要大量的运行时特性支持,因此目前还没有任何对它的支持计划。“对于代码的隔离,我们建议通过进程或容器实现。而对于程序集的动态加 载,我们建议使用新的AssemblyLoadContext类。”

例如以下代码:

// .NetFramework
public void assemblyTestMethod()
{
  var _asm = Assembly.Load("C:\Test.dll");
}
// .Net Core
public void assemblyTestMethodCore()
{
    var _asm = AssemblyLoadContext.Default.LoadFromAssemblyPath("C:\Test.dll");
}

NO.2 GetCustomAttribute() 扩展支持

在.NetFramework当中,GetCustomAttribute()方法是针对Type与TypeInfo对象的,但是在.NetCore当中如果要调用这个
方法的话,则仅能通过Type.GetTypeInfo().GetCustomAttribute()来获得对象的特性标签。

NO.3 GetInterface与GetInterfaces方法

貌似在.NetCore当中的Type对象是没有Getinterface方法的,So…你只有使用后者获得一个IEnumerable对象再来筛选啦。

.Net Core 通过依赖注入和动态加载程序集实现宿主程序和接口实现类库完全解构..._weixin_34128839的博客-CSDN博客

mikel阅读(560)

来源: .Net Core 通过依赖注入和动态加载程序集实现宿主程序和接口实现类库完全解构…_weixin_34128839的博客-CSDN博客

网上很多.Net Core依赖注入的例子代码,例如再宿主程序中要这样写:

services.AddTransient<Interface1, Class1>();

其中Interface1是接口,Class1是接口的实现类,一般我们会将接口项目和实现类项目分开成两个项目以实现解耦。

但这段代码却要求宿主程序要引用实现类项目,所以这里的解构实现的并不彻底,要完全解耦就是要实现宿主程序不引用实现类项目。
或者把注入的代码改成这样:

 services.Add(new ServiceDescriptor(serviceType: typeof(Interface1),
                                          implementationType: Type.GetType("ClassLibrary1.Class1, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"),
                                          lifetime: ServiceLifetime.Transient));

其实这段代码也要求宿主类引用实现类库项目,不然运行会出错,只有采用动态加载程序集的方式才能实现宿主程序不引用实现类:

 var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\Users\pesu\source\repos\DynamicLoadDependencyInjectionConsoleApp\ClassLibrary1\bin\Debug\netcoreapp2.0\ClassLibrary1.dll");

上面代码将实现类库程序集动态加载到宿主程序,然后将注入的代码改成这样:

  services.Add(new ServiceDescriptor(serviceType: typeof(Interface1),
                                          implementationType: myAssembly.GetType("ClassLibrary1.Class1"),
                                          lifetime: ServiceLifetime.Transient));

其中ClassLibrary1.Class1是Interface1的实现类,完整代码如下:

using InterfaceLibrary;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Runtime.Loader;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\Users\pesu\source\repos\DynamicLoadDependencyInjectionConsoleApp\ClassLibrary1\bin\Debug\netcoreapp2.0\ClassLibrary1.dll");
         
            IServiceCollection services = new ServiceCollection();
            //注入
            services.AddTransient<ILoggerFactory, LoggerFactory>();
           
            services.Add(new ServiceDescriptor(serviceType: typeof(Interface1),
                                          implementationType: myAssembly.GetType("ClassLibrary1.Class1"),
                                          lifetime: ServiceLifetime.Transient));

            //构建容器
            IServiceProvider serviceProvider = services.BuildServiceProvider();
            //解析
            serviceProvider.GetService<ILoggerFactory>().AddConsole(LogLevel.Debug);
            var cls = serviceProvider.GetService<Interface1>();
            cls.Say();
            Console.ReadKey();
        }
    }
}

输出:


这有什么用呢?

ASP.NET Core系列:读取配置文件 - libingql - 博客园

mikel阅读(636)

来源: ASP.NET Core系列:读取配置文件 – libingql – 博客园

1. 控制台应用

新建一个控制台应用,添加两个Package:

Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.Json

1.1 单个配置文件

创建配置文件appsettings.json:

{
  "Data": "10000",
  "ConnectionStrings": {
    "DevContext": "开发库",
    "ProdContext": "生产库"
  }
}

读取配置文件:

using System;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");
            var config = builder.Build();

            Console.WriteLine(config["Data"]); // 配置键
            Console.WriteLine(config.GetSection("ConnectionStrings:DevContext").Value); // 分层键
        }
    }
}

通过ConfigurationBuilder对象来创建ConfigurationRoot对象,进行读取配置文件。

SetBasePath:设置配置文件基础路径

AddJsonFile:添加读取的Json文件

var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json");

配置文件可选及修改自动加载设置:

var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json");

配置键约定:

(1)键不区分大小写。 例如,ConnectionString 和 connectionstring 被视为等效键。

(2)分层键使用冒号分隔符 (:)

1.2 多个配置文件

AddJsonFile:添加多个配置文件。

var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true);

{
  "Data": "10000",
  "ConnectionStrings": {
    "DefaultContext": "默认库"
  }
}

{
  "Data": "10000",
  "ConnectionStrings": {
    "DefaultContext": "开发库"
  }
}
using System;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile("appsettings.Development.json");
            var config = builder.Build();

            Console.WriteLine(config.GetConnectionString("DefaultContext")); // 输出:开发库
        }
    }
}
using System;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.Development.json")
                .AddJsonFile("appsettings.json");
            var config = builder.Build();

            Console.WriteLine(config.GetConnectionString("DefaultContext")); // 输出:默认库
        }
    }
}

配置键相同时,读取选择最后一个添加的文件(AddJsonFile)。

若要读取所有添加文件的配置信息,可遍历ConfigurationRoot的Providers属性。

using System;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile("appsettings.Development.json");
            var config = builder.Build();

            foreach (var provider in config.Providers)
            {
                provider.TryGet("ConnectionStrings:DefaultContext", out string defaultContext);
                Console.WriteLine(defaultContext);
            }

            // 输出:默认库
            //      开发库
        }
    }
}

1.3 配置读取绑定

(1)Key读取(键值对)

{
  "AppID": 10000,
  "Node": {
    "ID": "1",
    "Text": "节点"
  }
}
var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json");
var config = builder.Build();

var appID = config["AppID"];
var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json");
var config = builder.Build();

var nodeid = config.GetSection("Node").GetSection("ID").Value;

(2)绑定简单数据类型 GetValue<T>

添加Package:Microsoft.Extensions.Configuration.Binder

using System;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");
            var config = builder.Build();

            var appID = config.GetValue<int>("AppID");
            Console.WriteLine(appID);
        }
    }
}

(3)绑定到类对象  Get<T>

using System;
using System.Collections.Generic;
using System.Text;

namespace Libing.Core.ConsoleApp
{
    public class Node
    {
        public int ID { get; set; }

        public string Text { get; set; }
    }
}
using System;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");
            var config = builder.Build();

            var node = config.GetSection("Node").Get<Node>();

            Console.WriteLine("{0}-{1}", node.ID, node.Text);
        }
    }
}

(4)绑定集合对象

{
  "Node": {
    "ID": 1,
    "Text": "节点",
    "Children": [
      {
        "ID": 11,
        "Text": "子节点1"
      },
      {
        "ID": 12,
        "Text": "子节点2"
      },
      {
        "ID": 13,
        "Text": "子节点3"
      }
    ]
  }
}
using System;

using System.Collections.Generic;

using System.IO;
using Microsoft.Extensions.Configuration;

namespace Libing.Core.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(cfg =>
                {
                    cfg.Path = "appsettings.json";
                    cfg.Optional = false;
                    cfg.ReloadOnChange = true;
                });
            var config = builder.Build();

            var children = config.GetSection("Node:Children").Get<IEnumerable<Node>>();
            foreach (var child in children)
            {
                Console.WriteLine("{0}-{1}", child.ID, child.Text);
            }
        }
    }
}

2. API应用程序

新建API应用程序,在根目录下自动创建的Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

其中,CreateDefaultBuilder创建默认配置,包括:

(1)加载IConfiguration配置,读取文件appsettings.json;

(2)加载IConfiguration配置,读取文件appsettings.[EnvironmentName].json;

(3)……

2.1 ConfigureServices中键值读取

在根目录下文件Startup.cs中ConfigureServices()中采用键值对方式读取:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    int appid = Configuration.GetValue<int>("AppID");
    var nodes = Configuration.GetSection("Node:Children").Get<IEnumerable<Node>>();
}

2.2 ConfigureServices中使用Options

添加安装包:Microsoft.Extensions.Options.ConfigurationExtensions

{
  "Node": {
    "ID": 1,
    "Text": "NodeText"
  }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddOptions();
    services.Configure<Node>(Configuration.GetSection("Node"));
}

在控制器中,通过构造函数注入,读取配置。

public class LogController : ControllerBase
{
    private Node node;
    public LogController(IOptions<Node> option)
    {
        node = option.Value;
    }

    [HttpGet]
    public Node Get()
    {
        int id = node.ID;
        string text = node.Text;

        return node;
    }
}

2.3 读取自定义配置文件

在项目根目录下,新建配置文件 config.json:

{
  "AppID": "10000"
}

修改Program.js,增加读取config.json文件。

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddJsonFile("config.json", optional: true, reloadOnChange: true);
        })
        .UseStartup<Startup>();
}

在Startup.cs中调用:

public void ConfigureServices(IServiceCollection services)
{
    // 省略其它代码...
    string appid = Configuration.GetSection("AppID").Value;
}

2.4 独立类中读取配置

在.Net Framework中,ConfigurationManager类用于读取配置文件。但在.Net Core中没有该类,新建类ConfigurationManager用于读取配置文件。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Libing.Portal.Core.Api.Common.Configs
{
    public static class ConfigurationManager
    {
        public readonly static IConfiguration Configuration;

        static ConfigurationManager()
        {
            Configuration = new ConfigurationBuilder()
               .SetBasePath(Directory.GetCurrentDirectory())
               .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
               .Build();
        }

        public static T GetSection<T>(string key) where T : class, new()
        {
            return new ServiceCollection()
                .AddOptions()
                .Configure<T>(Configuration.GetSection(key))
                .BuildServiceProvider()
                .GetService<IOptions<T>>()
                .Value;
        }

        public static string GetSection(string key)
        {
            return Configuration.GetValue<string>(key);
        }
    }
}