![]() |
风度翩翩的莲藕 · .NET Aspire ...· 1 月前 · |
![]() |
重感情的哑铃 · docker逃逸的几种方法以及其原理· 2 周前 · |
![]() |
礼貌的绿茶 · 什么是Connection和Channel_ ...· 8 月前 · |
![]() |
逼格高的自行车 · 边学边用Gradle:Gradle的脚本结构 ...· 1 年前 · |
![]() |
果断的沙滩裤 · 工业视觉智能_机器视觉检测_工业缺陷检测_企 ...· 1 年前 · |
![]() |
帅气的枇杷 · 如何在WPF中绑定逆布尔属性?-腾讯云开发者 ...· 1 年前 · |
![]() |
烦恼的黑框眼镜 · 压测工具平台案例库-腾讯云开发者社区-腾讯云· 1 年前 · |
曾其何时docker-compose非常适合开发、测试、快速验证原型,这个小工具让单机部署容器变得简洁、高效。正如我在《docker-compose,docker-stack前世今生》里讲,所有人都认为docker-compose是单机部署多容器的瑞士军刀,没有docker stack由deploy配置节体现的生产特性(多实例、滚动部署、故障重启、负载均衡)。最近我发现我错了:docker-compose还是具备服务多实例的能力的。❝在docker-compose -h中发现了一个scale参数,这是个啥?docker-compose还能水平扩展,实现多容器?docker-compose定义的容器映射的主机端口不会冲突吗?❞号主精心分析,才找到一个完备的理论来支持scale参数的合理性。在此文中,我们将演示一个示例,说明如何使用Docker Compose运行服务的多实例version: "3" services: webapp: image: "luksa/kubia" depends_on: ports: - "8080:8080" # 主机Port: 容器暴露Port在此文件中,我们定义了一个webapp服务(nodejs程序在8080端口监听)为webapp容器定义了端口映射:从容器8080端口映射到主机的8080端口,这样我们可以在主机上使用http://localhost:8080URL访问服务器。Docker Compose --scale flag当我们运行docker-compose up -h命令时, 其中--scale选项显示为服务指定多实例--scale SERVICE=NUM Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.很显然,使用目前的DockerCompose配置运行docker-compose up --scale webapp=3将导致failed: port is already allocated错误:问题在于,我们试图运行webapp服务的三个实例,并将它们全部映射到主机同一端口,而「主机的8080端口只能绑定给一个容器」。解决错误的一种方法是将Docker Compose文件中的端口映射更改为- "8080", 这会将容器的端口8080暴露给主机上的临时未分配端口。这个操作延伸出另一个问题:在启动容器之前,我们将不知道用于访问服务的端口。要列出端口映射,请在运行docker-compose up --scale webapp=3之后运行docker-compose ps来查看容器:Name Command State Ports ------------------------------------------------------------- test_webapp_1 node app.js Up 0.0.0.0:32828->8080/tcp test_webapp_2 node app.js Up 0.0.0.0:32830->8080/tcp test_webapp_3 node app.js Up 0.0.0.0:32829->8080/tcp添加负载均衡器为了能够在不知道特定容器的端口的情况下访问webapp服务,并使用负载均衡机制将请求分发到容器,我们需要在容器堆栈中添加负载均衡器。在此示例中,将使用nginx作为负载均衡器:来完成对外接收、对内转发。在与docker-compose.yml文件相同的目录中创建以下nginx.conf文件,代理&转发请求user nginx; events { worker_connections 1000; http { server { listen 80; location / { proxy_pass http://webapp:8080; }这将配置nginx将请求从主机端口80转发到 http://webapp:8080。然后将由Docker’s embedded DNS解决寻址:该DNS服务器使用轮询实现来根据服务名称解析DNS请求,并将其分发给Docker容器。❝由于nginx服务负责对外接收请求、对内转发,因此webapp服务可不直接对外暴露。实际上我们可以从Docker Compose文件中删除webapp端口映射配置,而仅将端口8080通知给链接的nginx服务。❞version: "3" services: webapp: image: "luksa/kubia" nginx: image: nginx:latest volumes: - type: bind source: /home/root/test/nginx.conf target: /etc/nginx/nginx.conf depends_on: - webapp ports: - "80:80"通过此配置,我们现在可以利用Docker Compose工具的scale水平扩展、实现服务多实例。docker-compose up -d --scale webapp=3CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 05b024964274 luksa/kubia "node app.js" 15 minutes ago Up 15 minutes test_webapp_1 2fb56a22810a luksa/kubia "node app.js" 15 minutes ago Up 15 minutes test_webapp_3 84041c727b6e luksa/kubia "node app.js" 15 minutes ago Up 15 minutes test_webapp_2 3882beae8b56 nginx:latest "nginx -g 'daemon of…" 15 minutes ago Up 15 minutes 0.0.0.0:80->80/tcp test_nginx_1总结输出docker-compose利用Docker引擎内嵌DNS,提炼出水平扩展容器、服务多实例的能力 (用一个代理就能应用这个能力)Docker引擎内嵌DNS也是docker-compose利用服务名发现其他容器的关键在需要测试具备水平扩展能力的web服务时,docker-compose up -d --scale 提供了一种快速、简便的途径。以后谁再说docker-compose没有水平扩展容器、服务多实例的时候,就把这篇文章丢给他。
引言接上文,容器内web程序一般会绑定到http://0.0.0.0:{某监听端口}或http://+:{某监听端口},以确保使用容器IP可以访问到web应用。正如我们在ASP.NET Core官方镜像显示的,ASP.NET Core程序在容器内80端口监听请求This image sets the ASPNETCORE_URLS environment variable to http://+:80 which means that if you have not explicity set a URL in your application, via app.UseUrl in your Program.cs for example, then your application will be listening on port 80 inside the container.http://+:80是什么鬼?请求为什么会被路由到监听http://+:80地址的web服务器?UrlPrefix这里涉及一个不为人知的概念:UrlPrefixUrlPrefix是统一资源定位符Url的前缀部分:scheme://host:port/relativeURI"https://www.adatum.com:80/vroot/""https://adatum.com:443/secure/database/""http://+:80/vroot/"web程序启动后,根据监听地址UrlPrefix中的主机元素,会向系统组件Http Server API注册不同的路由桶,由Http Server API将接收的请求路由到合适的web程序。容器内web程序监听http://+:80地址,+ 是强通配符,意味着web程序在容器(轻量级虚拟机)内以任意主机名监听80端口的请求。监听地址UrlPrefix 中的主机元素有四种形态:强通配符 ( + )当主机元素是一个加号(+),UrlPrefix匹配所有可能的主机名,这时的UrlPrefix属于强通配符类别。强通配符在如下场景下有用:当web程序要忽略请求到达的方式或忽略请求host标头中指定的站点时,web服务器监听地址的主机元素可设置为强通配符+显式主机名当主机元素是完全限定的域名,web服务器的主机元素直接与传入请求的host标头相匹配, 明确的主机名对于多站点很有用,这些Web站点根据请求所指向的站点传递不同的内容。绑定IP的弱通配符主机元素为IP地址,这种类型的UrlPrefix匹配尚未与以上强通配符或显式主机名匹配的任意IP地址主机名弱通配符 ( * )当星号*作为主机元素出现时, 这种类型的UrlPrefix将会匹配尚未与以上强通配符、显式或IP绑定的弱通配符匹配的任意主机名, 此主机元素可以用作默认的catch-all,也可以用于指定URL名称空间的较大部分,而不必使用许多UrlPrefixesHttp Server API维护了一张路由表,决定哪一个应用程序接收传入请求,这张路由表是从预留数据库中构建的,当新产生一个注册项或预留项,将会被放进与特定主机元素相关的路由桶路由桶优先级当多个web程序监听的UrlPrefix有重叠时,Http Server API会根据注册的1-->4路由桶依次匹配,路由桶中UrlPrefix的相对URI部分中最长的匹配(假设URL的主机,端口和方案部分完全匹配)是最佳匹配。在路由桶中找到匹配项后,路由算法将停止搜索并跳过所有优先级较低的存储桶。例如下面的注册项:注册项: https://+:80/vroot/ is registered by app1注册项: https://adatum.com:80/ is registered by app2注册项: https://*:80/ is registered by app3对https://adatum.com:80/vroot/subdir/file.htm/的传入请求路由给 app1,对https://adatum.com:80/default.htm/的传入请求路由给 app2,对https://otheradatum.com:80/file.htm/的传入请求路由给 app3总结HTTP Sever API 提供了将请求路由到web程序的机制应用程序监听地址UrlPrefix的主机元素决定了路由策略,其中+强通配符 表示忽略请求主机名和请求的方式,可以认为是囫囵吞枣的接收满足(scheme、port、relativeUrl)的请求。多个web程序监听的UrlPrefix有重叠时,Http Server API根据host元素形成的路由桶有优先级这应该是一篇很冷门的知识点,但是结合我们的实际和理论,相信能给读者的知识结构添砖加瓦。
背景5年前容器技术扑面而来,如今已经成为面向云原生开发的基础架构,基于微服务的设计需要部署大量容器,同时强调了友好快速的管理容器。是时候推荐一个轮子Portainer.io:提供GUI界面的容器管理工具,给开发者的工具箱又增加了一个炫酷又实用的瑞士军刀。Portainer.io的优势轻量级 (2,3个命令就可启动,镜像少于30M)健壮、 友好可以用于Docker监控和构建提供Docker环境的详细信息可在界面管理 Container、Image、Network、Volume、ConfigPortainer.io特性漂亮的Dashboard,很容器操作和监视许多内置的操作模板尽乎实时的 监视Container、Image...支持Docker-Swarm 集群监视安装Portaniner.io为Porttainer.io 创建Volumesudo docker volume create portainer_data启动portainer容器,配置在宿主机9000端口映射sudo docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainerUnable to find image 'portainer/portainer:latest' locallylatest: Pulling from portainer/portainerd1e017099d17: Pull completef4f2fd75fb8a: Pull completeDigest: sha256:026381c60682b82a863f0c3737a9b4a414beaddd4cf050477a7749ff5ac61189Status: Downloaded newer image for portainer/portainer:latest82756791026adda45c288ca465ef38ca2e2aefaad2b27da6ae3831a517db4ad8“请确保OS防火墙允许9000端口访问睁眼看Portainer.io首次访问请注册用户我是在Docker宿主机上安装的portainer.io,故我选择Local概览如下:点击任意一个红框对象,进入‘Dashboard Endpoint summary’简单的Docker监控在Container标签页使用container命令操作测试容器:“这个页面会显示所有的容器,包括Stopped,可使用docker system prune -a:Remove all unused images not just dangling ones点击每个容器,可进入查看容器的详细信息:Container status/Container health/Container details/Connected networks/VolumesImage标签页,这里显示所有Image,这类可以拉取、构建、导入导出镜像根据模板快速创建服务堆栈 点击‘'App Template’, 进入容器构建页面。选择WordPress模板操作之后,可生成新的服务堆栈:“是不是很6,这个服务堆栈已经使用WordPress模板预置,配置详情查看Update页面设定的远程docker-stack.yml地址:https://github.com/portainer/templates/blob/master/stacks/wordpress/docker-stack.ymlversion: '3'services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_DATABASE_PASSWORD} MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress wordpress: image: wordpress:latest ports: - 80 restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress volumes: db_data:其他Stack、Service、Network、Volume、Config请自行倒腾, 外围配置Extension,Registries 可配置安全特性和 镜像注册中心。以上就是本文的全部内容,希望这个Portainer GUI文章有助于您更有效地管理和监视容器。真诚的希望得到您的反馈。
背景这几天在研究Kubernetes, 遇到一个有意思的nodejs镜像:luksa/kubia# 不带端口映射启动容器docker run -it -d luksa/kubia# 连接到默认的Bridge网桥,容器IP是 172.17.0.2之后,在宿主机使用容器IP和8080 端口可访问该容器nodejs服务对此我有几个疑问,这几个疑问在我看来有点与我之前对docker 网络的认知相冲突。Q1. 不是说如果容器没有端口映射,容器内外隔离吗,怎么在宿主机使用容器IP还可以访问?Q2. 使用容器IP:8080可以访问nodejs服务,这个8080从哪里来?头脑风暴首先排除一些同事说法:这个容器是以host网络模型连到宿主机,所以可以在宿主机通过容器IP访问。这个新建容器肯定还是连接到默认的bridge网桥上。All containers without a --network specified, are attached to the default bridge network.In terms of Docker, a bridge network uses a software bridge which allows containers connected to the same bridge network to communicate, while providing isolation from containers which are not connected to that bridge network.对于Q1,我有个误区:没有端口映射,容器内外网络隔离,宿主机是无法访问容器的。A: 实际上,对于加入同一bridge网桥上的容器,网桥内外网络确实是隔离的,网桥上的容器都可以相互连接。而我们的宿主机也在这个默认的bridge网桥设备上,其IP地址是网桥设备的网关(172.17.0.1)。Q3.那端口映射到底起什么作用呢?A:网桥模型确保了网桥内容器可相互访问,但除此网桥之外的网络均不能访问容器, 这也正是bridge网络隔离的效果。端口映射-p表示容器绑定宿主机的网卡端口来实现转发访问,绑定的网卡决定了你对外暴露的程度。绑定宿主机的回环地址127.0.0.1docker run -it -d -p 127.0.0.1:8080:8080 luksa/kubia那么在宿主机内只能使用127.0.0.1:8080访问容器绑定宿主机的物理地址 10.201.80.126docker run -it -d -p 10.201.80.126:8080:8080 luksa/kubia那么可使用宿主机物理IP10.201.80.126:8080访问容器,这样局域网机器就能访问到容器了3. 不写IP,这样会绑定到0.0.0.0,也就是宿主机所有的网卡。docker run -it -d -p 8080:8080 luksa/kubia很显然,宿主机内回环地址和物理地址均可以访问该容器了。再回到上面的Q2问题,通过容器IP:8080访问容器,8080是哪里来的?8080是容器内nodejs进程的监听端口,我们在构建镜像时本就无所谓使用expose指令The EXPOSE instruction does not actually publish the port. It functions as a type of documentation between the person who builds the image and the person who runs the container, about which ports are intended to be published.所以在docekr ps时候,并不会在PORTS列显示任何内容,但是通过容器IP可直接连通容器内进程监听端口。为啥访问容器IP:8080 就可以访问容器内nodejs提供的服务?这是因为容器镜像在构建的时候,一般在0.0.0.0地址上监听请求,这意味着程序在所有地址的8080端口上监听请求。这样就延伸出一个有趣的现象,让我们进入容器内部:docker exec -it 3cc9f428fc25 bash curl 127.0.0.1:8080 curl 127.0.0.2:8080 curl 127.0.1:8080 curl 172.17.0.2:8080 curl 172.17.2:8080几个看起来错误的IP竟然也可以访问nodejs服务, 这正是nodejs在http://0.0.0.0:8080地址监听请求的结果。# 截取自该镜像构建源码:https://github.com/luksa/kubia-qps/blob/master/kubia-qps/app.jsvar www = http.createServer(handler);www.listen(8080); # nodejs: server.listen([port[, host[, backlog]]][, callback]) apiIf host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available, or the unspecified IPv4 address (0.0.0.0) otherwise.猜想+ 验证+ 源码支持,回应了一开始的几个疑问,对容器Bridge的网络认知进一步加深。总结输出bridge网桥内容器通过容器IP相互访问,外部网络隔离docker run -p 参数通过端口映射,让bridge网桥外网络可以访问容器一般情况下,对外提供web服务的docker镜像会在0.0.0.0 地址上监听请求
开发者工具1. Web DeveloperWeb Developer 这款扩展集成了各种各样的 Web 开发工具,几乎是网页开发人员必备的 Chrome 开发者工具扩展插件,Web Developer插件的工具栏很多,Web Developer 主要由以下几个部分组成:Disable、Cookies、CSS、Forms、Images、Information、Miscellaneous、Outline、Resize、Tools、View Source和Options, 网页功能之强大,令人发指。2. FeHelper这款是开发者工具的集大成者,不仅包含前端实用的工具,如JSON美化、代码美化、JS正则表达式、栅格规范检测、网页性能检测、页面取色等web常见功能,还包含工作小工具:MarkDown转换、Crontab工具、字符串编解码、二维码编解码、编码规范检测、便捷思维导图、我的便签,对编辑、广告、媒体行业从业者也相当有效。3.Postman Interceptor(需要先安装Postman Chrome App)前后端数据联调的鼻祖,无人不知无人不晓,目前Postman官方已经推荐安装原生Postman App,但本人还是喜欢在浏览器在使用这个调用器,拉起Potman App。4.EditThisCookieEditThisCookie是一个cookie管理器。您可以添加,删除,编辑,搜索,锁定和屏蔽cookies. 作为一名掌控欲极强的开发者,能自由掌握cookie,那种感觉真的很自在。针对程序员,推荐两款有效的Github插件5. Octotree要想成为大神,Github就是最佳样板,而原生的Github代码文件浏览很不方便,所以找到了Octotree。Octotree是一个显示Github项目目录结构优秀插件。Octotree的特性:1.类似 IDE 的非常方便的代码目录树2.使用 PJAX 的超快代码浏览(很快!)3.支持公有库和私有库6. sourcegraph这是一款让你像使用IDE那样浏览代码,跳转定义、鼠标悬停查看签名、查看引用,搭配Octotree真是让你在Github查看代码的时候大呼过瘾。日常效率工具类7. 划词翻译顾名思义,网页上鼠标划词翻译,支持谷歌、百度、有道三大翻译和朗读引擎,可以方便的查看、复制和朗读不同引擎的翻译结果不管你信不信,反正翻译效果超过本人脑汁翻译,在下佩服。8.LastPass现在网站密码、身份密码、金融密码这么多,保存同步密码是刚性需求, 这款LastPass是我对比1Password(收费贵), PasswordBox(倒闭)之后极力推荐的,稳定使用3年,核心的多终端同步功能于2016年免费使用。9.Nimbus日常跟技术怼、跟产品怼,跟QA怼,没图你说个JB.这款插件让您在网页截图,截屏、截滚动屏、录制视频,还能延迟截屏, 截完就开怼。10.AdBlock号称最佳广告拦截工具,安装之后,整个世界清爽了。浏览器管理类11. OneTabChrome作为电脑吃内存大户,特别是开了很多标签之后,电脑卡的一逼。OneTab通过将标签全部折叠,一键节省95%的内存,当然也可以一键恢复。12.谷歌访问助手在vpn日趋严格的今天,Ctrl+C/V程序员依旧需要面向谷歌编程,这款谷歌访问代理插件,本人已经稳定使用三年(购买的vpn掉链子),本次偷偷将破解版(无广告)分享给大家。开发者心里要有数,这本质是一款浏览器网络代理,在排查某些网络问题时需要关注这一点。 13.百度药丸让百度世界更纯粹,安装之后再也不用费心心力明辨是非。1.屏蔽百度推广2.阻止百度追踪3.美化首页(需退出百度账号)总结不敢贪多,就这13款,横跨工作效率、日常工具、浏览器管理,这13款插件让您自由飞翔。从Chrome网上应用店下载这些牛逼插件需要能访问谷歌站点,所以这又是一个鸡生蛋还是蛋生鸡的问题,幸好Chrome提供离线安装的能力。
默认搭建的副本集均在主节点读写,辅助节点冗余部署,形成高可用和备份,具备自动故障转移能力。集群心跳保活集群每个节点以周期性向其他成员发出心跳命令 replSetHeartbeat 来获取状态,根据应答消息来更新节点的状态,根据最终状态确定是否重选主节点。默认心跳周期 heartbeatIntervalMillis= 2000ms;认定Primary节点失联的阈值 electionTimeoutMillis=10s异步复制辅助节点复制主节点的oplog,并将改变应用到数据集,从而保持与主节点数据同步。、这里有三个知识点:oplog是一个特殊的封顶集合capped collection, 主节点上的operation log会记录在主节点的oplog中,辅助节点异步拷贝这些操作,这样所有的节点的都包含operatin log的一个副本:local.oplog.rs集合每次异步复制触发的时机是在心跳保活阶段,所有的辅助节点都会在ping阶段从其他成员插入oplog文档。oplog中的每个操作都是冥等的:无论是一次还是多次应用到目标数据集,oplog操作会产生相同的结果删除和插入操作若多次应用删除操作,后续删除操作无效果;若多次应用插入操作,因为每次操作均包含包含_id值,因此它也不会插入文档的第二个副本(因为_id必须是唯一的)。当有新节点加入集群,该节点会启动另一种同步复制:initial sync, 将所有数据从某副本集成员完全拷贝, 复制完成,会过渡为辅助节点。选举主节点集群会因为各种事件触发选举主节点在集群中添加新节点初始化replica set集群执行人工运维命令(rs.stepDown() rs.reconfig())维护集群辅助节点与主节点失联时间超过默认10s自动故障转移说的是最后一种情况:默认情况下,辅助节点A与主节点心跳失联超过10s,A节点标记主节点不可用;之后与其他辅助节点心跳保活,沟通各自信息(节点的票数、节点优先级、PingMs等因素)确立出新主节点。在发生故障转移时,集群不能再执行写入操作;若客户端配置在辅助节点读取(read preference),则集群可继续提供读取能力。你的应用程序可用重试逻辑应对自动故障转移和后续的重选。从MongoDB3.6版本开始,MongoDB Driver可侦测主节点的失联,并执行一次重试操作。tip适配MongoDB4.2的Driver默认会重试写入操作;适配Mongodb4.0-3.6的Driver需显式在连接字符串包含retryWrites = true,以确保主节点失联时能重试写入操作。连接副本集的配置字符串,其中rs0是集群配置文件中 replSetName。mongodb://account:passward@mongodb0.example.com:27017,mongodb1.example.com:27017,mongodb2.example.com:27017?replicaSet=rs0OK, 以上便是MongoDB副本集心跳保活、异步复制、自动故障转移的背景知识。
副本集Replica Set是一个术语,定义具有多节点的数据库集群,这些节点具有主从复制(master-slave replication) 且节点之间实现了自动故障转移。这样的结构通常需要具有奇数个成员的成员(无论是否带有Arbiter节点),以确保正确地选择PRIMARY(主)数据库。选定的DB将处理所有传入的写操作,并将有关它们的信息存储在其oplog,每个辅助(从属)副本成员都可以访问和复制oplog,以应用于它们的数据集。前置为创建一个Replica set, 至少需要三个MongoDB实例,请查看官网安装指南.本文会始终使用sudo指令,一般情况请为MongoDB服务创建一个标准用户 mongod配置网络为达到数据一致性,每个实例节点需要与集群其他节点通信,以三实例数据传输为例:① replica set每个成员都使用私有IP,部署在同一数据中心,这也是推荐方式。② replica set每个节点使用公网ip,节点部署在不同数据中心(在replication时有网络延迟),这种方式一般用于强灾备部署,如果采用这种方式,需要在主机之间配置SSL/TLS或通过vpn通信replica set节点认证本节你会使用openssl创建一个用于在副本集成员之间认证的key文件,MongoDB推荐使用x.509证书加密连接。① 产生key文件openssl rand -base64 756 > mongo-keyfile 将生成的key文件拷贝到复制集的每个成员② 确保复制集成员都能访问同一路径的key文件:sudo mkdir /opt/mongo sudo mv ~/mongo-keyfile /opt/mongo sudo chmod 400 /opt/mongo/mongo-keyfile③ 默认安装的MongoDB使用标准账户 mongod ,确保mongod对文件有所有权sudo chown mongod:mongod /opt/mongo/mongo-keyfile创建Admin用户登陆你打算设为Primary的MongoDB节点,进入admin数据库,创建具有root特权的管理员用户use admin db.createUser({user: "mongo-admin", pwd: "password", roles:[{role: "root", db: "admin"}]})配置MongoDB修改复制集每个成员的mongod.conf:net: port: 27017 bindIp: 127.0.0.1,192.0.2.1 security: keyFile: /opt/mongo/mongo-keyfile replication: replSetName: rs0指定key文件、replication set名称; 重启服务sudo systemctl restart mongod启动集群,添加节点使用之前创建的管理员账户登陆 Primary MongoDB服务节点:mongo -u mongo-admin -p --authenticationDatabase admin① 初始化集群添加节点rs.initiate() rs.add("mongo-repl-2") rs.add("mongo-repl-3")以上使用hostsname 代替节点ip地址,需要在节点/etc/hosts添加节点的 hosts映射条目。② 使用rs.conf() 或 rs.status() 验证集群配置和状态倒腾Replica Set完成以上步骤,MongoDB 三实例Replica Set已经搭建好了。登陆Primary节点做一些常规倒腾 (顺便捡漏一些你意想不到的姿势)① 输入测试数据use exampleDB for (var i = 0; i <= 10; i++) db.exampleCollection.insert( { x : i } )将会隐式创建exampleDB 和 文档集合exampleCollection。请注意, 默认创建的Collection是不封顶的。封顶capped collection:有固定大小的集合,支持高通量操作,这些操作根据插入顺序插入和检索文档, 以循环缓冲区的形式工作(一旦集合达到分配空间,会通过override旧文档来腾挪出新的空间)。② 观察Secondary节点是否已经同步到插入的数据 使用创建的管理员账户登陆 Secondary节点,直接查询会报:因为默认建立的Replica set读写均发生均在Primary节点(Secondary节点的作用是:冗余备份、故障转移);不过MongoDB replica set支持在客户端设置read preference(读操作首选项),大部分Driver均支持在连接字符串中指定read preference读操作首选项,这个设置可实现真正意义的master-slave读写分离。对应到shell会话,我们需要为本次Secondary会话 开启可读db.getMongo().setSlaveOk()或使用shell命令的读取首选项回过头来实操本文,你已经可以完整搭建MongoDB Replica Set,大致掌握了副本集的核心特性:主节点读写、辅助节点冗余备份;支持对辅助节点开启读操作。
启用 HTTPS 还不够安全 现在很多站点通过HTTPS对外提供服务,用户在访问某站点,往往会直接输入站点域名(baidu.com),而不是完整的HTTPS地址(https://www.baidu.com),站点一般会发送301重定向,要求浏览器升级到HTTPS连接。将所有非安全请求重定向到安全URL是常规做法,但是中间人仍然可以在重定向发生前劫持连接。HSTS指示浏览器只能使用HTTPS访问域名,来处理潜在的中间人劫持风险。即使用户输入或使用普通的HTTP连接,浏览器也严格将连接升级到HTTPS。HSTSHSTS是一种可选的安全增强策略,已经由IETF RFC6797中指定。服务端通过Strict-Transport-Security响应头来通知客户端应用HSTS协议:Strict-Transport-Security: max-age=31536000; includeSubDomains若浏览器认可该响应头:浏览器为该域名存储(阻止请求使用HTTP连接)这一约定,浏览器将强制所有请求通过 HTTPS浏览器阻止用户使用不安全/无效证书,会显示禁用提示(允许用户临时信任该证书)因为HSTS策略由客户端强制执行,有一些前置条件:客户端必须支持 HSTS 协议必须要有一次成功的HTTPS请求,这样才能建立HSTS 策略Preload HSTS细心的你可能发现,HSTS还是存在一个薄弱漏洞,那就是浏览器没有当前HSTS信息,或者第一次访问;或者新操作系统,浏览器重装,清除浏览器缓存;HSTS信息的max-age过期;依然需要一次明文HTTP请求和重定向才能升级到HTTPS并刷新HSTS信息,这一次依然给攻击者可乘之机,针对以上攻击,HSTS的应对办法是在浏览器内置一个域名列表,这个列表内域名,浏览器都会使用HTTPS发起连接,这个列表由Chrome维护,主流浏览器均在使用。一旦浏览器认可这个响应头,知晓访问这个域名的所有请求必须使用HTTPS连接,将会在1年时间内缓存这个约定。inclueSubDomains 是可选参数,告知浏览器将HSTS策略用到当前域的子域。Nginx启用HSTS在Nginx中设置 HSTS 相对简单:add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;# always 参数确保所有的响应都有 STS Header, 旧版本(低于1.7.5)不支持always参数。nginx add_header 的继承规则:如果某个配置块包含一个add_header 指令,那么将不会继承上层的headers, 因此你需要在内部配置块重申 add_header 指令。server { listen 443 ssl; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # This 'location' block inherits the STS header location / { root /usr/share/nginx/html; } # Because this 'location' block contains another 'add_header' directive, # we must redeclare the STS header location /servlet { add_header X-Served-By "My Servlet Handler"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; proxy_pass http://localhost:8080; }}ASP.NETCore的福利时间若使用Kestrel作为边缘(face-to-internet) web服务器,相关配置可参考AddHsts()的lambda参数:为STS header设置preload参数,Preload不是RFC HSTS规范的一部分,但是浏览器支持在全新安装时预加载HSTS网站指定子域使用HSTS协议, 或排除某些子域使用HSTS设置浏览器缓存 [访问站点的请求均使用HTTPS协议] 这一约定的时间,默认是30天。public void ConfigureServices(IServiceCollection services){ services.AddMvc(); services.AddHsts(options => { options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(60); options.ExcludedHosts.Add("example.com"); options.ExcludedHosts.Add("www.example.com"); }); services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect; options.HttpsPort = 5001; });}请注意:UseHsts对于本地回送hosts并不生效localhost: IPv4回送地址127.0.0.1 IPv4回送地址[::1] IPv6回送地址这也是开发者在localhost:5001启动时抓不到Strict-Transport-Security 响应头的原因。下面给出启用了HSTS的生产示例:+ nginx启用HSTS: https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/+ chrome清除HSTS信息: https://www.ssl2buy.com/wiki/how-to-clear-hsts-settings-on-chrome-firefox-and-ie-browsers
背景上次Redis MQ分布式改造之后,编排的容器稳定运行一个多月,昨天突然收到ETL端同事通知,没有采集到解析日志。赶紧进服务器 docker ps查看容器:用于数据接收的ReceiverApp容器挂掉了;尝试docker container start [containerid],几分钟后该容器再次崩溃。 Redis连接超限docker log [containerid] 查看容器日志: 显示连接Redis服务的客户端数量超限。CSRedis.RedisException: ERR max number of clients reached.Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] Executed action EqidManager.Controllers.EqidController.BatchPutEqidAndProfileIds (EqidReceiver) in 7.1767msfail: Microsoft.AspNetCore.Server.Kestrel[13] Connection id "0HLPR3AP8ODKH", Request id "0HLPR3AP8ODKH:00000001": An unhandled exception was thrown by the application.CSRedis.RedisException: ERR max number of clients reached at CSRedis.CSRedisClient.GetAndExecute[T](RedisClientPool pool, Func`2 handler, Int32 jump, Int32 errtimes) at CSRedis.CSRedisClient.ExecuteScalar[T](String key, Func`3 hander) at CSRedis.CSRedisClient.LPush[T](String key, T[] value) at RedisHelper.LPush[T](String key, T[] value) at EqidManager.Controllers.EqidController.BatchPutEqidAndProfileIds(List`1 eqidPairs) in /home/gitlab-runner/builds/haD2h5xC/0/webdissector/datasource/eqid-manager/src/EqidReceiver/Controllers/EqidController.cs:line 31 at lambda_method(Closure , Object ) at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult() at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at System.Threading.Tasks.ValueTask`1.get_Result() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync() at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 8.9549ms 500 【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached)【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached)【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached)【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached)【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached) 【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached)【dockerhost:6379/0】仍然不可用,下一次恢复检查时间:09/17/2019 03:11:25,错误:(ERR max number of clients reached)快速思考:目前编排的某容器使用CSRedisCore对16个Redis DB实例化了16个客户端,但Redis服务也不至于这么不经折腾吧。赶紧进redis.io官网搜集资料。After the client is initialized, Redis checks if we are already at the limit of the number of clients that it is possible to handle simultaneously (this is configured using the maxclients configuration directive, see the next section of this document for further information).In case it can't accept the current client because the maximum number of clients was already accepted, Redis tries to send an error to the client in order to make it aware of this condition, and closes the connection immediately. The error message will be able to reach the client even if the connection is closed immediately by Redis because the new socket output buffer is usually big enough to contain the error, so the kernel will handle the transmission of the error.大致意思是:maxclients配置了Redis服务允许的客户端最大连接数, 如果当前连接的客户端数超限,Redis服务会回发一个错误消息给客户端,并迅速关闭客户端连接。立刻进入Redis宿主机查看默认配置,确认当前Redis服务的maxclients=10000(这是一个动态值,由maxclients和最大进程文件句柄决定)。# Set the max number of connected clients at the same time. By default# this limit is set to 10000 clients, however if the Redis server is not# able to configure the process file limit to allow for the specified limit# the max number of allowed clients is set to the current file limit# minus 32 (as Redis reserves a few file descriptors for internal uses).## Once the limit is reached Redis will close all the new connections sending# an error 'max number of clients reached'.# maxclients 10000通过Redis-Cli登录Redis服务器, 立刻被踢下线。基本可认定Redis客户端使用方式有问题。CSRedisCore使用方式查看Redis官方资料,可利用redis-cli命令info clients、client list 分析客户端连接。info clients 命令显示现场确实有10000的连接数;client list命令输出字段的官方解释:addr: The client address, that is, the client IP and the remote port number it used to connect with the Redis server.fd: The client socket file descriptor number.name: The client name as set by CLIENT SETNAME.age: The number of seconds the connection existed for.idle: The number of seconds the connection is idleflags: The kind of client (N means normal client, check the full list of flags).omem: The amount of memory used by the client for the output buffercmd: The last executed command以上解释表明Redis服务器收到很多ip=172.16.1.3(故障容器在网桥内的Ip 地址)的客户端连接,这些连接最后发出的是ping命令(这是一个测试命令)故障容器使用的Redis客户端是CSRedisCore,该客户端只是单纯将msg写入Redis list数据结构,CSRedisCore上相关github issue给了一些启发。发现自己将CSRedisClient实例化代码写在 .NETCore api Controller构造函数,这样每次请求构造Controller时都实例化一次Redis客户端,最终Redis客户端连接数达到最大允许连接值。依赖注入三种模式: 单例(系统内单一实例,一次性注入);瞬态(每次请求产生实例并注入);自定义范围。有关dotnet apiController 以瞬态模式注入,请查阅文末链接。还有一个疑问?为什么Redis服务器没有释放空闲的客户端连接,如果空闲连接被释放了,即使我写了low代码也不至于如此?查询官方:By default recent versions of Redis don't close the connection with the client if the client is idle for many seconds: the connection will remain open forever.However if you don't like this behavior, you can configure a timeout, so that if the client is idle for more than the specified number of seconds, the client connection will be closed.You can configure this limit via redis.conf or simply using CONFIG SET timeout .大致意思是最新的Redis服务默认不会释放空闲的客户端连接。# Close the connection after a client is idle for N seconds (0 to disable)timeout 0修改以上Redis服务配置可释放空闲客户端连接。我们最佳实践当然不是修改Redis idle timeout 配置,问题本质还是因为我实例化了多客户端,赶紧将CSRedisCore实例化代码移到startup.cs并注册为单例。大胆求证info clients命令显示稳定在53个Redis连接。client list命令显示:172.16.1.3(故障容器)建立了50个客户端连接,编排的另一个容器webapp建立了2个连接,redis-cli命令登录到服务器建立了1个连接。那么问题来了,修改之后,ReceiverApp容器为什么还稳定建立了50个redis连接?进一步与CSRedisCore原作者沟通,确认CSRedisCore有预热机制,默认在连接池中预热了 50 个连接。bingo,故障和困惑全部排查清楚。总结经此一役,在使用CSRedisCore客户端时,要深入理解① Stackexchange.Redis 使用的多路复用连接机制(使用时很容易想到注册为单例),CSRedisCore开源库采用连接池机制,在高并发场景下强烈建议注册为单例, 否则在生产使用中可能会误用在瞬态请求中实例化,导致redis连接数几天之后消耗完。② CSRedisCore会默认建立连接池,预热50个连接,开发者要心中有数。额外的方法论: 尽量不要从某度找答案,要学会问问题,并尝试从官方、stackoverflow、github社区寻求解答,你挖过的坑也许别人早就挖过并踏平过。
温故知新目前常见的Http请求明文传输,请求可能被篡改,访问的站点可能被伪造。HTTPS是HTTP加上TLS/SSL协议构建的可进行加密传输、身份认证的网络协议,主要通过数字证书、加密算法、非对称密钥等技术完成互联网数据传输加密,实现互联网传输安全保护。流程解读① 传输密钥是对称密钥,用于双方对传输数据的加解密② 怎么在传输之前确立传输密钥呢?答:针对普遍的多客户端访问受信web服务器的场景, 提出非对称密钥(公钥下发给客户端,私钥存于web服务器),双方能互相加解密,说明中间数据(传输密钥)没被篡改。③ 再抛出疑问,客户端如何认定下发的公钥是目标web服务器的公钥?又如何确定公钥下发过程没被截取篡改?答:追溯到握手阶段的证书验证过程,浏览器从证书提取(证书颁发机构,证书绑定的域名,证书签名,证书有效期);浏览器先验证证书绑定的域名是否与目标域名匹配;浏览器内置证书颁发机构认定该证书是其有效下发;通过签名认定该证书没被篡改。④ 所以浏览器内置的证书机构(根证书)的权威性很重要, 中毒或山寨浏览器可能携带非法的根证书。如果面向面试记忆Https原理,恐怕有些难度,所以个人用一种 【鸡生蛋还是蛋生鸡】的方式向上追溯流程, 方便大家知其然更知其所以然。下面演示对ASP.NET Core程序两种常见部署模型强制应用Https。常规反向代理模型由nginx反向代理请求到后端https://receiver.server, 在nginx上添加HTTPS证书, 并强制使用HTTPS。worker_processes 4;events { worker_connections 1024; }http { sendfile on; upstream receiver_server { server receiver:80; } server { listen 80; listen [::]:80; server_name eqid.******.com; return 301 https://eqid.******.com$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; ssl on; server_name eqid.******.com; ssl_certificate /conf.crt/live/******.com.crt; ssl_certificate_key /conf.crt/live/******.com.key; location / { proxy_pass http://receiver_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_redirect off; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }}dotnet.exe自宿模型Kestrel用作边缘(面向Internet)Web服务器, 这个部署模型不常见,但依旧存在。我们利用 Visual Studio 2019项目模板构建 ASP.NetCore项目--- 勾选HTTPS支持, 会默认添加支持Https的Middleware;app.UseHttpsRedirection() 强制Http请求跳转到Httpsapp.UseHsts() 指示浏览器为特定主机头在特定时间范围内的所有通信应用Https。HSTS(HTTP Strict Transport Protocol)的作用是强制浏览器使用HTTPS与服务器创建连接,避免原有的301重定向Https时可能发生中间人劫持。服务器开启HSTS的方法是,当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议响应头中包含Strict-Transport-Security字段。非加密传输时设置的HSTS字段无效。Development证书VS模板构建的web会使用dotnet cli 提供的开发证书在https://localhost:5001 地址接收请求。关于开发证书, 可倒腾 dotnet dev-certs https --help 命令:dotnet dev-certs https -c清除证书,启动程序会报无服务器证书异常;dotnet dev-certs https -t信任证书,会弹窗提示确认安装名为localhost的开发根证书:- 否:web能正常启动,Https请求将获取无效证书,浏览器地址栏警示▲不安全(提示浏览器不信任localhost根证书,证书无效)- 是:web正常启动,浏览器发在地址栏显示正常的Httsp小锁♎图标在Windows上,最安全方式是使用certificate store来注册已认证的HTTPS,但是有时候希望在程序内绑定证书+私钥, 这样便于在不同平台上部署。文件证书ASP.NET Core支持使用硬盘上文件证书来建立Https连接(这在linux上很常见)。以下代码允许Kestrel传入文件证书和私钥,并建立Https连接。public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseKestrel(options => { options.Listen(IPAddress.Loopback, 5000); options.Listen(IPAddress.Loopback, 5001, listenOptions => { listenOptions.UseHttps("certificate.pfx", "topsecret"); }); }) .UseStartup<Startup>();务必确保不要将私钥存储在配置文件中:在开发模式,可使用user secrets 存储此类密钥;在生产模式,可考虑Azure Key Vault或环境变量。总结希望本文有助于您大致了解ASP.NET Core中Https的应用方式。这不是什么高深的理论,而是尝试以不同的方式启用Https、并着重解释相关中间件的用法。
ASP.NET Core设计初衷是开源跨平台、高性能Web服务器,其中跨平台特性较早期ASP.NET是一个显著的飞跃,.NET现可以理直气壮与JAVA同台竞技,而ASP.NET Core的高性能特性更是成为致胜法宝。ASP.NET Core 2.1+为IIS托管新增In-Process模型并作为默认选项(使用IISHttpServer替代了Kestrel,dotnet程序由IIS网站进程w3wp.exe内部托管)。为展示ASP.NET Core跨平台特性,本文重点着墨经典的Out-Process托管模型。宏观设计为解耦平台web服务器差异,程序内置Http服务组件Kestrel,由web服务器转发请求到Kestrel。老牌web服务器定位成反向代理服务器,转发请求到ASP.NET Core程序(分别由IIS ASP.NET Core Module和Nginx负责)常规代理服务器,只用于代理内部主机对外网的连接需求,一般不支持外部对内部网络的访问请求; 当一个代理服务器能够代理外部网络的主机,访问内部网络,这种代理服务器被称为反向代理服务器 。平台web代理服务器、ASP.NET Core程序(dotnet.exe) 均为独立进程,平台自行决定互动细节,只需确保平台web服务器与Kestrel形成Http通信。Kestrel与老牌web服务器解耦,实现跨平台部署。Kestrel使ASP.NET Core具备了基本web服务器的能力,在内网部署和开发环境完全可使用dotnet.exe自宿模式运行。Kestrel定位是Http服务组件,实力还比不上老牌web服务器,在timeout机制、web缓存、响应压缩等不占优势,在安全性等方面还有缺陷。因此在生产环境中建议使用老牌web服务器反向代理请求。跨平台管控程序,转发请求要实现企业级稳定部署:*nix平台将ASP.NET Core程序以dotnet.exe自宿模式运行,并配置为系统守护进程(管控应用),再由Nginx转发请求。以下使用systemd创建进程服务文件 /etc/systemd/system/kestrel-eqidproxyserver.service[Unit] Description=EqidProxyServer deploy on centos [Service] WorkingDirectory=/var/www/eqidproxyserver/eqidproxyServer ExecStart=/usr/bin/dotnet /var/www/eqidproxyserver/eqidproxyServer/EqidProxyServer.dll Restart=always # Restart service after 10 seconds if the dotnet service crashes: RestartSec=10 TimeoutStopSec=90 KillSignal=SIGINT SyslogIdentifier=dotnet-example User=root Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false [Install] WantedBy=multi-user.target// 启用服务,在localhost:5000端口侦听请求 sudo systemctl enable kestrel-eqidproxyserver.service安装Nginx,并配置Nginx转发请求到localhost:5000:server { listen 80; server_name default_website; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { proxy_pass http://localhost:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }Windows平台[ 管控应用、转发请求] 由ASP.NET Core Module(插入在IIS Pipeline中的原生组件,下面简称ACM)一手操办,w3wp.exe、dotnet.exe的互动关系是通过父子进程维系。下图脚本力证dotnet.exe进程是w3wp.exe创建出来的子进程:得益此关系,ACM在创建dotnet.exe子进程时能指定环境变量,约定donet.exe接收(IIS转发的请求)的侦听端口。实际源码看ACM为子进程设定三个重要的环境变量:ASPNETCORE_PORT 约定 Kestrel将会在此端口上监听ASPNETCORE_APPL_PATASPNETCORE_TOKEN 约定 携带该Token的请求为合法的转发请与ACM夫唱妇随的是UseIISIntegration()扩展方法,完成如下工作:① 启动Kestrel服务在http://localhost:{ASPNETCORE_PORT}上监听② 根据 {ASPNETCORE_TOKEN} 检查请求是否来自ACM转发ACM转发的请求,会携带名为MS-ASPNETCORE-TOKEN:******的Request Header,以便dotnet.exe对比研判。③ 利用ForwardedHeaderMiddleware中间件保存原始请求信息linux平台部署需要手动启用ForwardedHeader middleware https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.1源码快速验证:namespace Microsoft.AspNetCore.Hosting{ public static class WebHostBuilderIISExtensions { // These are defined as ASPNETCORE_ environment variables by IIS's AspNetCoreModule. private static readonly string ServerPort = "PORT"; private static readonly string ServerPath = "APPL_PATH"; private static readonly string PairingToken = "TOKEN"; private static readonly string IISAuth = "IIS_HTTPAUTH"; private static readonly string IISWebSockets = "IIS_WEBSOCKETS_SUPPORTED"; /// <summary> /// Configures the port and base path the server should listen on when running behind AspNetCoreModule. /// The app will also be configured to capture startup errors. public static IWebHostBuilder UseIISIntegration(this IWebHostBuilder hostBuilder) { var port = hostBuilder.GetSetting(ServerPort) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{ServerPort}"); var path = hostBuilder.GetSetting(ServerPath) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{ServerPath}"); var pairingToken = hostBuilder.GetSetting(PairingToken) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{PairingToken}"); var iisAuth = hostBuilder.GetSetting(IISAuth) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{IISAuth}"); var websocketsSupported = hostBuilder.GetSetting(IISWebSockets) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{IISWebSockets}"); bool isWebSocketsSupported; if (!bool.TryParse(websocketsSupported, out isWebSocketsSupported)) { // If the websocket support variable is not set, we will always fallback to assuming websockets are enabled. isWebSocketsSupported = (Environment.OSVersion.Version >= new Version(6, 2)); } if (!string.IsNullOrEmpty(port) && !string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(pairingToken)) { // Set flag to prevent double service configuration hostBuilder.UseSetting(nameof(UseIISIntegration), true.ToString()); var enableAuth = false; if (string.IsNullOrEmpty(iisAuth)) { // back compat with older ANCM versions enableAuth = true; } else { // Lightup a new ANCM variable that tells us if auth is enabled. foreach (var authType in iisAuth.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) { if (!string.Equals(authType, "anonymous", StringComparison.OrdinalIgnoreCase)) { enableAuth = true; break; } } } var address = "http://127.0.0.1:" + port; hostBuilder.CaptureStartupErrors(true); hostBuilder.ConfigureServices(services => { // Delay register the url so users don't accidentally overwrite it. hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, address); hostBuilder.PreferHostingUrls(true); services.AddSingleton<IServerIntegratedAuth>(_ => new ServerIntegratedAuth() { IsEnabled = enableAuth, AuthenticationScheme = IISDefaults.AuthenticationScheme }); services.AddSingleton<IStartupFilter>(new IISSetupFilter(pairingToken, new PathString(path), isWebSocketsSupported)); services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); services.Configure<IISOptions>(options => { options.ForwardWindowsAuthentication = enableAuth; }); services.AddAuthenticationCore(); }); } return hostBuilder; } }}总结 ASP.NET Core跨平台的核心在于 程序内置Kestrel HTTP通信组件,解耦web服务器差异。本文从框架设计初衷、进程模型、组件交互验证我对ASP.NET Core跨平台特性的理解。
引言.Net TPL Dataflow是一个进程内数据流管道,应对高并发、低延迟的要求非常有效, 但在实际Docker部署的过程中, 有一个问题一直无法回避:单体程序部署的瞬间(服务不可用)会有少量流量无法处理;更糟糕的情况下,迭代部署的这个版本有问题,上线后无法工作, 导致更多流量没有处理。 背负神圣使命(巨大压力)的程序猿心生一计,为何不将单体程序改成分布式:增加服务ReceiverApp,ReceiverApp只接受数据,WebApp只处理数据。知识储备消息队列和订阅发布作为老生常谈的两个知识点被反复提及,按照JMS的规范, 官方称为点对点(point to point, queue)和发布/订阅(publish/subscribe,channel)点对点 生产者发送消息到Message Queue中,然后消费者从队列中取出消息并消费。队列会保留消息,直到他们被消费或超时;① MQ支持多消费者,但每个消息只能被一个消费者处理② 发送者和消费者在时间上没有依赖性,当发送者发送消息之后,不管消费者有没有在运行(甚至不管有没有消费者),都不会影响到消息被发送到队列③ 一般消费者在消费之后需要向队列应答成功如果希望发送的每个消息都被成功处理,你应该使用p2p模型发布/订阅消息生产者将消息发布到Channel,在此之前已有多个消费者订阅该通道。和点对点方式不同,发布到特定通道的消息会被通道订阅者实时接收。通道没有队列机制,发布的消息只能被当前收听的订阅者接收到① 每个消息可以有多个订阅者② 发布者和消费者有时间上依赖性:某通道的订阅者,必须先创建该通道订阅,才能收到消息发布消息至通道,不关注订阅者是谁;订阅者可收听自己感兴趣的多个通道(类似于topic),也不关注发布者是谁。③ 故如果没有订阅者,发布的消息将得不到处理;头脑风暴Redis内置的List数据结构能形成轻量级消息队列的效果;Redis原生支持发布/订阅 模型如上分析, Pub/Sub模型在订阅者宕机的时候,发布的消息得不到处理,故此模型不能用于强业务的数据接收和处理。本次采用的消息队列模型:解耦业务:新建ReceiverApp作为生产者,专注于接收并发送到队列;原有的WebApp作为消费者专注数据处理。起到削峰填谷的作用,若缩放出多个WebApp消费者容器,还能形成负载均衡的效果。 需要关注Redis操作List结构的两个命令( 左进右出,右进左出同理): LPUSH & RPOP/BRPOPBrpop中的B 表示"Block",是一个rpop命令的阻塞版本:若指定List没有新元素,在给定时间内,该命令会阻塞当前redis客户端连接,直到超时返回nilAspNetCore编程实践本次使用AspNetCore 完成RedisMQ的实践,引入Redis国产第三方开源库CSRedisCore生产者ReceiverApp生产者使用LPush命令向Redis List数据结构写入消息。------------------截取自Startup.cs------------------------- public void ConfigureServices(IServiceCollection services) // Redis客户端要定义成单例, 不然在大流量并发收数的时候, 会造成redis client来不及释放。另一方面也确认api控制器不是单例模式, var csredis = new CSRedisClient(Configuration.GetConnectionString("redis")+",name=receiver"); RedisHelper.Initialization(csredis); services.AddSingleton(csredis); services.AddMvc(); ------------------截取自数据接收Controller-------------------[Route("batch")] [HttpPost] public async Task BatchPutEqidAndProfileIds([FromBody]List<EqidPair> eqidPairs){ if (!ModelState.IsValid) throw new ArgumentException("Http Body Payload Error."); var redisKey = $"{DateTime.Now.ToString("yyyyMMdd")}"; eqidPairs = await EqidExtractor.EqidExtractAsync(eqidPairs); if (eqidPairs != null && eqidPairs.Any()) RedisHelper.LPush(redisKey, eqidPairs.ToArray()); await Task.CompletedTask; }消费者WebApp 根据以上RedisMQ思路,事件消费方式是拉取pull,故需要轮询Redis List数据结构,这里使用AspNetCore内置的BackgroundService后台服务类后台轮询消费:关注后台Job中的循环接收方法。public class BackgroundJob : BackgroundService private readonly IEqidPairHandler _eqidPairHandler; private readonly CSRedisClient[] _cSRedisClients; private readonly I Configuration _conf; private readonly ILogger _logger; public BackgroundJob(IEqidPairHandler eqidPairHandler, CSRedisClient[] csRedisClients,IConfiguration conf,ILoggerFactory loggerFactory) _eqidPairHandler = eqidPairHandler; _cSRedisClients = csRedisClients; _conf = conf; _logger = loggerFactory.CreateLogger(nameof(BackgroundJob)); protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogInformation("Service starting"); if (_cSRedisClients[0] == null) _cSRedisClients[0] = new CSRedisClient(_conf.GetConnectionString("redis") + ",defaultDatabase=" + 0); RedisHelper.Initialization(_cSRedisClients[0]); while (!stoppingToken.IsCancellationRequested) var key = $"eqidpair:{DateTime.Now.ToString("yyyyMMdd")}"; var eqidpair = RedisHelper.BRPop(5, key); if (eqidpair != null) await_ eqidPairHandler.AcceptEqidParamAsync(JsonConvert.DeserializeObject<EqidPair>(eqidpair)); // 强烈建议无论如何休眠一段时间,防止突发大流量导致WebApp进程CPU满载,自行根据场景设置合理休眠时间 await Task.Delay(10, stoppingToken); _logger.LogInformation("Service stopping"); }迭代验证使用docker-compose单机部署Nginx,ReceiverApp,WebApp容器。docker-compose up指令默认只会重建[Service配置或Image变更]的容器。If there are existing containers for a service, and the service’s configuration or image was changed after the container’s creation, docker-compose up picks up the changes by stopping and recreating the containers (preserving mounted volumes). To prevent Compose from picking up changes, use the --no-recreate flag.做一次迭代验证,更新docke-compose.yml文件WebApp服务的镜像版本,docker-compose up;下图显示仅 数据处理容器 WebApp被Recreate:Nice,分布式改造完成,效果很明显,现在可以放心安全的迭代核心WebApp数据处理程序。
长话短说开发者都希望从部署在Azure的App Services中压榨出最佳性能,更好的性能不仅能够获得更佳的响应体验,而且如果性能提升的策略在Azure中能有“四两拨千斤”的效果,那么性能提升还可以为我们省钱。在本文,我们将研究提高Azure App Services中运行的Web程序性能的设置和策略。下面几个性能提升意见在App Service配置界面即可操作,这一组技巧的主题是评估程序现状,压榨出程序本身性能。1. 启动HTTP/2Microsoft于2018年初宣布在App Services中支持HTTP/2,但到目前为止在Azure中默认创建的App Service还是以HTTP1.1协议工作。HTTP/2对常见的的Web协议进行了重大更改,许多更改旨在提高性能并减少Web延迟 (例如HTTP/2中的标头压缩和二进制格式将减少有效负载大小);另外请求管道和多路复用等功能允许使用更少的网络套接字来执行更多并发请求,并有助于避免一个缓慢的请求阻止所有后续请求,这在HTTP1.1是常见问题。如上图示,为你的的App Service启用HTTP/2协议,下拉列表指定HTTP2.0版本后,所有支持HTTP/2的客户端都将自动升级其连接,不支持HTTP/2的客户端仍然以原有Http1.1 方式交互。下面是一个简单的测试以验证HTTP/2的改进:某App Service托管页面引用了脚本、CSS资源、16张图像(每个图像的大小超过200 KB),使用developer tool记录使用HTTP 1.1在App Service发生的情况。注意观察条形红色部分显示了后置请求以阻塞状态开始。这是可怕的“行头阻塞”问题,其中[对连接数和并发请求的限制] 制约了客户端和服务器之间的吞吐量,直到第一个请求开始后800毫秒,客户端才会收到该页面的最终字节。接下来在App Service中启用了HTTP/2支持:不需要对客户端或服务器上进行任何其他配置更改,不到500ms完成所有请求。由于HTTP/2提高了网络利用率,从而避免了阻塞。2. 关闭空闲休眠如果你有将应用程序部署到IIS的经历,那么你应该知道IIS在一段时间不活动之后将休眠(这个配置在IIS理默认是20分钟)。Azure App Service延续了这一传统。尽管休眠可为在同一App Service Plan上运行的其他App Service提供资源,但是此策略会损害当前应用程序的性能,因为下一个传入请求将经历Web服务器冷启动的过程:缓存为空、连接池为空,站点预热,所有请求的速度都比正常情况慢。为了防止空闲休眠,你可以在"App Service配置"中【始终开启】标志。3. 关闭App Service实例亲和力即使你仅运行App Service Plan的单实例,每个Azure App Service前面都是负载平衡器,负载均衡器会转发请求到App Service实例。当App Service因流量缩放出多实例,负载均衡器使用Application Request Routing将连接会话分发给实例。因为Azure无法知晓应用程序是不是stateless服务,故默认的App Service将确保客户端在会话期间访问同一App Service实例,为了实现这种亲和力,负载均衡器会在对客户端的第一个响应中注入ARRAffinity Cookie。如果你的应用程序是stateless,并允许负载平衡器在实例之间分配请求,请关闭请求路由cookie,以提高性能和弹性。下面的改进需要一些其他网络规划或重组(某些情况下,还需要更改应用程序本身)\这一组技巧中的主题是缩短数据在网络上传输的距离4. 让你的服务资源相距更近比如常规的WebApi服务,需要搭建App Service和Database,建议你把资源放在同一区域协同工作,不然一次请求,处理链路会满世界跑。5. 让你的App Service与使用者更接近如果大多数客户流量都来自世界的特定区域,则将资源放置在离客户最近的Azure区域中是很有意义的。当然,我们许多人的客户分布在世界各地。在这种情况下,您可以考虑跨多个Azure区域进行地理复制,以与每个人保持更近距离,之后你使用类似Azure Traffic Manager(基于DNS技术的负载均衡器)将你的客户直接路由到最近的服务实例。6. 让你的服务内容与使用者更接近脚本、图片、CSS,视频等静态资源是在CDN边缘服务器上缓存的较好选择,一旦缓存,Azure App Service不需要花费带宽和时间在这些资源上,专注处理动态资源。回过头来,看以上性能优化建议,第一步还是要评估App Service当前现状和性能,不是每一个策略都对你的App Service有效。btw 这些策略对于常规企业级部署依旧有所指引。
长话短说经过长时间实操验证,终于完成基于Gitlab的CI/CD实践,本次实践的坑位很多, 实操过程尽量接近最佳实践(不做hack, 不做骚操作),记录下来加深理解。看过博客园《docker-compose真香》一文的园友留意到文中[把部署dll文件拷贝到生产机器],现场打包成镜像并启动容器,并没有完成CI/CD. P1:Gitlab CI/CD原理和Gitlab Runner安装(这里使用shell执行器) P2:基于Docker-compose的Gitlab CI/CD 实践:宏观业务架构图.gitlab-ci.yml文件项目部署文件Gitlab CI/CD部署准备Gitlab CI/CD原理Gitlab CI/CD 存储[构建]、[构建状态]的api应用程序, 提供友好的管理界面, 构建过程由 .gitlab-ci.yml文件定义(该文件一般置于代码仓库的根目录)Gitlab Runner 执行构建任务的应用程序,可独立部署,如上图所示其通过api与Gitlab Server交互搭建Gitlab CI/CD环境Gitlab CI/CD提供配置界面(项目菜单栏-设置-CI/CD),可指定 将要使用何种形式的Runner 配置Runner要用到环境变量界面配置权限取决于你在Gitlab Server的角色 + https://docs.gitlab.com/ee/user/permissions.html本次手动设置特定Gitlab Runner:Runner安装完毕,注册Runner(与Gitlab Projects实例建立绑定关系)注册时要关注的两个配置:Tags 与此Runner相关的任务标签, 用于在共享Runner中区分不同的Project,.gitlab-ci.yml会用到Runner Executor 执行构建任务的方式,这里使用shell方式Shell是最简单的配置执行器,需要将构建所需的所有依赖项手动安装在安装了Runner的同一台计算机上。注册过程和结果请参考下图:Gitlab CI/CD实践宏观业务架构图原则上不允许自动部署Prod,本次使用Gitlab Runner服务器作为Gitlab CD的部署机器。Gitlab-CI Pipeline构建ReceiverAPP、webAPP镜像(附带本次git:tag)并推送到hub.docker.com;Gitlab-CD docker-compose拉取远端nginx、ReceiveAPP、webapp镜像,启动容器。 Pipeline对每一次提交或合并都会执行build任务,形成Continous IntergationPipeline对git: tag会触发build_Image任务,成功之后构建deploy:staging任务,这样就能形成基于git:tag的部署版本管理(部署出错,也能很快回滚到上次的部署tag).gitlab-ci.yml文件 以上Gitlab Pipeline定义build->build_image->deploy3个任务,某些任务还包括不同分支Job,写.gitlab-ci.yml 的过程就是将以上执行动作脚本化。stages: - build - build_image - deploy variables: # CI_DEBUG_TRACE: "true" deploy_path: "/home/xxxx/eqidmanager" # CI变量,用于配置部署目录 before_script: - "docker info" build: stage: build script: - "for d in $(ls src);do echo $d;prog=$(pwd)/src/$d/$d.csproj; dotnet build $prog; done" tags: - another-tag build_image:EqidManager: stage: build_image script: - dotnet publish src/EqidManager/EqidManager.csproj -c release -o ../../container/app/publish/ - docker build --pull -t $CI_REGISTRY_USER/eqidmanager:$CI_COMMIT_REF_NAME container/app - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD - docker push $CI_REGISTRY_USER/eqidmanager:$CI_COMMIT_REF_NAME tags: - another-tag only: #Pipeline Job构建策略,代码仓库打tag会执行该任务, 支持正则 - tags build_image:EqidReceiver: stage: build_image script: - dotnet publish src/EqidReceiver/EqidReceiver.csproj -c release -o ../../container/receiver/publish - docker build -t $CI_REGISTRY_USER/eqidreceiver:$CI_COMMIT_REF_NAME container/receiver - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD - docker push $CI_REGISTRY_USER/eqidreceiver:$CI_COMMIT_REF_NAME tags: - my-tag only: - tags deploy:staging: stage: deploy script: - cd $deploy_path - export TAG=$CI_COMMIT_REF_NAME # 引入本次CI的git:tag名称,覆盖.env文件默认配置 - "docker-compose -f docker-compose.yml -f docker-compose.prod.yml build" - "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d" tags: - my-tag deploy:prod: stage: deploy script: - # TODO 需要写脚本登陆到Prod机器上 - export TAG=$CI_COMMIT_REF_NAME - cd $deploy_path - "docker-compose -f docker-compose.yml -f docker-compose.prod.yml build" - "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d" tags: - my-tag when: manual这里有些知识点、坑位需要指出:第8行:预先定义的环境变量,该变量定义gitlab CD的部署目录第16行: 对src开发目录下两个程序执行dotnet build命令第17行:tags定义具备该tags的Runner可以执行该任务,注意这里的tags必须是字符串数组第23-26行:构建镜像并推送到镜像仓库的过程,用到两类CI变量 - 密钥变量CI_REGISTRY_USER、CI_REGISTRY_PASSWORD,可在Gitlab-CI界面配置 - 预定义变量CI_COMMIT_REF_NAME,该变量标记构建项目的git:branch或git:tag名称,用于生成Image:Tag注意变量可被重写,重写优先级:http://www.ttlsa.com/auto/gitlab-cicd-variables-zh-document/第29行:only定义此Job只在产生git:tag时被触发,与上面我们使用CI-COMMIT_REF_NAME 变量相呼应第47行:Gialab-CI pipeline每个Job会重新拉取git源码执行Job任务(可登录到Gitlab Runner工作目录下观察Runner执行过程),CD时需要选择合适目录,这是deploy_staging上使用deploy_path CI变量的原因第48行:注入本次Gitlab-CI git:tag名称,实际上是覆盖了.env同名环境变量第49行:若存在docker-compose.yml、docker-compose.override.yml 两个文件,docker-compose命令会自动merge这2个文件(使用docker-compose config命令查看merge之后的结果)。第64行:前置任务未出错,会自动执行后继任务;而when指令定义该任务需要界面上手动执行 部署目录 在Gitlab Runner服务器的{deploy_path}路径下建立了如下部署文件:├── appsettings.secrets.json├── docker-compose.prod.yml├── docker-compose.yml├── .env├── EqidManager.db├── nginx│ ├── Dockerfile│ └── nginx.conf└── receiver.secrets.json部署目录定义docker-compose.yml、docker-compose.prod.yml 两个yml文件,前者定义常规容器服务,后者定义适用于本部署环境的附加服务密钥文件不要进入代码管理,因此我们定义appsetting.secrets.json 和 receiver.secrets.json密钥文件,由dccker-compose.yml挂载进入容器env文件存储相对固定且与本次docker-compose命令相关的环境变量,docker-compose命令默认寻找同级目录下.env文件------.env 文件----TAG=master # 该TAG变量会在Pipeline:deploy_staging任务中被覆盖,形成基于git:tag的imageName:tagdocker_host=172.16.1.1COMPOSE_PROJECT_NAME=EqidManagerDOCKER_REGISTRY=***Project打上git:tag之后,触发Gitlab Runner CI/CD Pipeline: 跳转到部署目录->应用本次git:tag->执行docker-compose命令拉取指定tag镜像并启动容器。That'all, 本次应用Gitlab Runner(shell执行器)实践CI/CD, Gitlab菜单界面有所有构建构成的日志(便于排查构建问题);另外上文对于关键知识均附带传送门,可进一步对比研究。
长话短说2C互联网业务增长,单机多核的共享内存模式带来的排障问题、编程困难;随着多核时代和分布式系统的到来,共享模型已经不太适合并发编程,因此actor-based模型又重新受到了人们的重视。---------------------------调试过多线程的都懂-----------------------------传统编程模型通常使用回调和同步对象(如锁)来协调任务和访问共享数据,从宏观看:若任务的执行需要某些共享资源,不可避免该任务需要关注并抢占资源。actor-based模型是一种流水线模型,actor-based模型share nothing。所有的线程(或进程)通过消息传递的方式进行合作,这些线程(或进程)称为参与者actor,预先定义任务流水线后,不关注数据什么时候流到这个任务,专注完成当前任务本身。 .Net TPL Dataflow组件帮助我们快速实现actor-based模型,当有多个必须异步通信的操作或要等待数据可用再进一步处理时,Dataflow组件非常有用。TPL Dataflow是微软前几年给出的数据处理库, 内置常见的处理块,可将这些块组装成一个处理管道,"块"对应处理管道中的"阶段任务",可类比AspNetCore 中Middleware和Pipeline。TPL Dataflow库为消息传递、CPU密集型/I-O密集型应用程序提供了编程基础, 可更明确控制数据的暂存方式、移动路线,达到高吞吐量和低延迟。需要注意的是:TPL Dataflow非分布式数据流,消息在进程内传递 。TPL Dataflow核心概念TPL Dataflow 内置的Block覆盖了常见的应用场景,如果内置块不能满足你的要求,你也可以自定“块”。Block可以划分为下面3类:Buffering Only [Buffer不是缓存Cache的概念,而是一个暂存区的概念]ExecutionGrouping 使用以上块混搭处理管道, 大多数的块都会执行一个操作,有些时候需要将消息分发到不同Block,这时可使用特殊类型的缓冲块给管道“”分叉”。Execution Block可执行的块有两个核心组件:输入、输出消息的暂存区(一般称为Input,Output队列)在消息上执行动作的委托消息在输入和输出时能够被暂存:当输入的消息速度比Func委托的执行速度比快,后续消息将在到达时暂存;当下一个块的输入暂存区中无可用空间,将在当前块输出时暂存。每个块我们可以配置:暂存区的总容量,默认无上限执行操作委托的并发度,默认情况下块按照顺序处理消息,一次一个。将块链接在一起形成处理管道,生产者将消息推向管道。TPL Dataflow有一个基于pull的机制(使用Receive和TryReceive方法),但我们将在管道中使用块连接和推送机制。TransformBlock(Execution category)-- 由输入输出暂存区和一个Func委托组成,输入的每个消息,都会输为出另一个,可以使用这个Block去执行消息的转换,或者转发输出的消息到另外一个BlockTransformManyBlock (Execution category) -- 由输入输出暂存区和一个Func>委托组成, 它为输入的每个消息输出一个IEnumerableBroadcastBlock (Buffering category)-- 只容纳最多1个消息的暂存区和Func委托组成(新消息到达会覆盖原消息),委托仅仅为了让你控制怎样克隆这个消息,不做消息转换该块在需要将消息广播给多个块时很有用(管道分叉)ActionBlock (Execution category)-- 由缓冲区和Action委托组成,它们不再给其他块转发消息,只处理输入的消息,一般作为管道结尾BatchBlock (Grouping category)-- 告诉它你想要的每个批处理的大小,它将累积消息,直到它达到那个大小,然后将它作为一组消息转发到下一个块其他内建Block类型:BufferBlock、WriteOnceBlock、JoinBlock、BatchedJoinBlock,暂时不会深入。管道连锁反应 当B块输入缓冲区达到上限容量,为其供货的上游A块的输出暂存区将开始被填充,当A块输出暂存区已满时,该块必须暂停处理,直到暂存区有空间,这意味着一个Block的处理瓶颈可能导致所有前面的块的暂存区被填满。 但是不是所有的块暂存区满时都会暂停,BroadcastBlock有1个消息的暂存区,每个消息都会被覆盖, 因此如果这个广播块不能及时将消息转发到下游,则在下个消息到达的时候消息将丢失,某种意义上达到一种限流效果(比较残暴).编程实践生产者投递消息 可使用Post或者SendAsync方法向首块投递消息:Post方法即时返回true/false,True意味着消息被block接收(暂存区有空余),false意味着拒绝了消息(暂存区已满或者Block已经出错)。SendAsync方法返回一个Task, 将会以异步的方式阻塞直到块接收、拒绝、块出错。Post、SendAsync的不同点在于SendAsync可以延迟投递(后置管道的输入buffer不空,得到异步通知后再投递)。定义流水线管道按照上图业务定义流水线:public EqidPairHandler(IHttpClientFactory httpClientFactory, RedisDatabase redisCache, IConfiguration con, LogConfig logConfig, ILoggerFactory loggerFactory) { _httpClient = httpClientFactory.CreateClient("bce-request"); _redisDB0 = redisCache[0]; _redisDB = redisCache; _logger = loggerFactory.CreateLogger(nameof(EqidPairHandler)); var option = new DataflowLinkOptions { PropagateCompletion = true }; publisher = _redisDB.RedisConnection.GetSubscriber(); _eqid2ModelTransformBlock = new TransformBlock<EqidPair, EqidModel> ( // redis piublih 没有做在TransformBlock fun里面, 因为publih失败可能影响后续的block传递 eqidPair => EqidResolverAsync(eqidPair), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = con.GetValue<int>("MaxDegreeOfParallelism") } ); // https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/walkthrough-creating-a-dataflow-pipeline _logBatchBlock = new LogBatchBlock<EqidModel>(logConfig, loggerFactory); _logPublishBlock = new ActionBlock<EqidModel>(x => PublishAsync(x) ); _broadcastBlock = new BroadcastBlock<EqidModel>(x => x); // 由只容纳一个消息的缓存区和拷贝函数组成 _broadcastBlock.LinkTo(_logBatchBlock.InputBlock, option); _broadcastBlock.LinkTo(_logPublishBlock, option); _eqid2ModelTransformBlock.LinkTo(_broadcastBlock, option); } 仿IIS日志写入组件异常处理上述程序在生产部署时遇到相关的坑位:在测试环境_eqid2ModelTransformBlock块委托函数稳定执行,程序并未出现异样; 部署到生产之后,该Pipeline运行一段时间就停止工作,一直很困惑。后来通过监测_eqid2ModelTransformBlock.Completion属性,发现该块在执行某次委托时报错,提前进入完成态。当TPL Dataflow不再处理消息且保证不再处理消息的时候,就被定义为 "完成态", IDataflow.Completion属性(Task对象)标记该状态,Task对象的TaskStatus枚举值描述此Block进入完成态的真实原因。 TaskStatus.RanToCompletion "成功完成" 在Block中定义的任务 TaskStatus.Fault 因未处理的异常导致"过早的完成" TaskStatus.Canceled 因取消操作导致 "过早的完成"官方资料表明:某块进入Fault、Cancel状态,都会导致该块提前进入“完成态”,但因Fault、Canceled进入的“完成态”会导致输入暂存区和输出暂存区被清空。After Fault has been called on a dataflow block, that block will complete, and its Completion task will enter a final state. Faulting a block, as with canceling a block, causes buffered messages (unprocessed input messages as well as unoffered output messages) to be lost.故需要严肃对待异常,一般情况下我们使用try、catch包含所有的执行代码以确保所有的异常都被处理。 本文作为TPL Dataflow的入门指南微软技术栈的可持续关注actor-based模型的流水线处理组件,应对单体程序中高并发,低延迟相当巴适。
长话短说上个月公司上线了一个物联网数据科学项目,我主要负责前端接收设备Event,并提供模型参数下载(数据科学团队会优化参数)。WebApp部署在Azure,模型参数使用Azure SQL Server存储。最近从灰度测试转向全量部署之后,日志中时常出现:SQL Session会话超限的报错。19/12/18 20:41:18 [Error].[Microsoft.EntityFrameworkCore.Query].[][0HLS3MS83SC3K:00000004].[http://******/api/v1/soc-prediction-model/all].[].[GetModeParameters] An exception occurred while iterating over the results of a query for context type 'Gridsum.SaicEnergyTracker.CarModelContext'.Microsoft.Data.SqlClient.SqlException (0x80131904): Resource ID : 2. The session limit for the database is 300 and has been reached. See 'http://go.microsoft.com/fwlink/?LinkId=267637' for assistance.Changed database context to 'saic-carmodel'.Changed language setting to us_english. at Microsoft.Data.ProviderBase.DbConnectionPool.CheckPoolBlockingPeriod(Exception e) at Microsoft.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection) at Microsoft.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection) at Microsoft.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection) at Microsoft.Data.ProviderBase.DbConnectionPool.WaitForPendingOpen()--- End of stack trace from previous location where exception was thrown --- at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnectionAsync(Boolean errorsExpected, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnectionAsync(Boolean errorsExpected, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected) at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.AsyncQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()排查Azure上使用的是SQL Server Basic Edition(好歹也是付费版),全量发布至今,日均SQL访问次数约为10000,查询了Azure SQL的使用限制文档:一句话:付费级别和计算资源大小决定了Azure SQL最大会话数/请求数。若要缓解,要么升级硬件资源,要么优化查询利用率。本次使用EFCore操作SQL Server的方式, 是官方默认用法: 依赖注入框架注册一个自定义的 DbContext类型 在Controller构造函数中获取 DbContext实例这意味着每次请求都会创建一个 DbContext实例, 可以想象到 ① 在高并发请求下,连接数不断累积,最终某时刻会超过 Azure 的连接限制数量。 ② 频繁创建和销毁 DbContext 实例,影响App Service自身性能。EFCore2.0 为DbContext引入新的注册方式:透明地注册了 DbContext实例池:services.AddDbContextPool<CarModelContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SQL"))); - 一如既往支持lambda方式注册连接字符串 - 默认的连接池数量为 128- 每次使用完DbContext不会释放对象,而是重置并回收到DBContextPoolWeb程序中通过重用池中DbContext实例可提高高并发场景下的吞吐量, 这在概念上类似于ADO.NET Provider原生的连接池操作方式,具有节省DbContext实例化成本的优点, 这也是EFCore2.0 其中一个性能亮点。这么重要的使用方式竟然不在 EFCore Doc指南中默认演示,真是一个坑。修改代码重新部署之后,历经几天测试,暂时未出现最开始的SqlException异常。验证回过头随机验证SQL Server会话中的有效连接数量:48SELECT DEC.session_id, DEC.protocol_type, DEC.auth_scheme, DES.login_name, DES.login_timeFROM sys.dm_exec_sessions AS DES JOIN sys.dm_exec_connections AS DEC ON DEC.session_id = DES.session_id;总结① 提示EFCore2.0新推出的DbContextPool特性,有效提高SQL查询吞吐量② 尝试使用SQL Server 内置脚本自证会话中有效连接数
背景 回顾docker-compose vs docker stack差异:① docker-compose是docker引擎之外的容器编排工具(Python实现),需要单独安装;docker stack 是docker引擎原生支持的容器编排技术(Go实现)② 两者都支持最新docker-compose.yml 版本3容器编排文件,部分指令有差异。③ docker-compose 能现场Build镜像,更适用于开发、测试时候单机迭代部署;docker stack须预先准备镜像,具备生产环境诸多特性。为提高项目服务可用性评价值(SLA),决心从docker-compose切换到docker stack生产部署。头脑风暴docker swarm 集群部署有如下优点和特性:集群管理和Docker Engine集成分散式设计,Swarm分为Manager,Worker, Manager节点故障不会影响Worker节点期望的状态协调多主机网络,overlay网络支撑不同主机之间容器通信服务发现负载平衡:集群节点负载均衡、服务容器负载均衡滚动更新、失败策略业务模型角度【Stack、Service、Container模型】定义了适用于生产的应用架构(支持副本集、重启策略、滚动更新、更新、回滚策略)task是Docker Swarm中最小部署单位,task与容器是一对一的关系service是一个或一组容器在生产环境的预期状态(也可说是一组task的集合),在Worker节点上执行;有两种模式(对应下面docker-stack.yml-deploy-mode配置节)(默认)replicated: 指定容器数量global: 每个节点一个容器(容器数量由可用节点决定) 服务发现(外部客户端连接到Swarm中暴露的服务),有两种模式(对应下面docker-stack.yml-deploy-endpoint_mode)(默认)vip: Docker Swarm为每个服务分配1个虚拟ip,服务后有多少节点、服务请求到哪个节点容器对于客户端是透明的,也就是由Docker Swarm负载均衡服务内容器dnsrr: Docker Swarm 为每个服务建立DNS记录,返回可用容器的ip列表, 客户端直接请求其中一个ip, 这种方式一般用于自建负载均衡器部署模型角度Docker Swarm以多主机模型支撑业务,对于开发者来说, 一个节点或多节点部署的配置流程是类似的。 Docker Swarm有3个重要的网络概念:① overlay network:覆盖物网络,在Docker宿主机底层网络之上搭建的分布式网络, 支撑不同主机之间容器的通信。在初始化或刚加入Swarm集群时,会创建以下ingress、docker-gwbridge网络② ingress network:入口网络,是一种特殊的overlay网络,外部客户端访问集群暴露的服务,在入口负载均衡(存在Swarm loadbancer将请求路由到可用节点容器)。③ docker-gwbridge: 将overlay网络上容器连接到docker宿主机的网络。以上可选配置都可以在docker-compose.yml 版本3官方文档找到对应的配置字段:deploy: endpoint_mode: 服务发现的方式:vip、 dnsrr labels:为服务指定的标签 mode:replicated、global replicas:实例数量 resources:配置资源 restart_policy:重启策略 update_config: 服务更新策略 parallelism:同时更新容器数量 delay:容器组更新的间隔时间 failure_action: 更新失败的操作:continue、rollbak,pause(默认) monitor:监视更新失败的等待时间 max_failure_ratio: 更新的失败容错率 order:操作策略:stop-first、start-first rollback_config:回滚策略 ...同上...走向集群改造目标三个服务-->nginx--> receiver-->app,容器之间通过{webnet} overlay网络通信;nginx开放外部访问端口80和8080,关注ingress网络receiver、app服务需要访问宿主机上搭建的Redis,关注docker-gwbridge网络一般两个步骤:① 搭建集群 ② 发布服务P1搭建Docker Swarm集群单节点/多节点的初始化方式:参考docker swarm -- help指令;集群节点的管理:参考docker node --help指令$ docker swarm --helpUsage: docker swarm COMMANDManage Swarm Commands: ca Display and rotate the root CA init Initialize a swarm join Join a swarm as a node and/or manager join-token Manage join tokens, 如果忘记Token,可以执行这个参数 leave Leave the swarm unlock Unlock swarm unlock-key Manage the unlock key update Update the swarmP2 docker stack发布服务可使用docker service create方式创建服务,个人偏好定义docker-stack.yml文件发布。下面在生产部署中追加的production.ymlversion: "3.7" services: proxy: networks: - webnet receiver: deploy: replicas: 1 restart_policy: condition: on-failure networks: - webnet deploy: replicas: 2 restart_policy: condition: on-failure update_config: parallelism: 1 delay: 5s order: stop-first networks: - webnet networks: webnet:# docker stack不加载同目录下的.env环境变量文件,原有适用于docker-compose工具的yml文件可采用变通方法docker stack deploy -c <(docker-compose -f docker-stack.yml -f production.yml config) eqidstack服务部署效果:注意其中的Ports指的是 服务对外暴露的端口#docker stack ls:NAME SERVICES ORCHESTRATOReqidstack 3 Swarm #docker service ls:ID NAME MODE REPLICAS IMAGE PORTS jml6ecfa330r eqidstack_app replicated 2/2 12205500/eqidmanager:master 3381stpkirgj eqidstack_proxy replicated 1/1 nginx:latest *:80->80/tcp, *:8080->8080/tcpvhz4ef8p4ffp eqidstack_receiver replicated 1/1 12205500/eqidreceiver:master可通过 docker network inspect ingress 验证容器eqidstack_proxy.1连接到ingress网络;docker network inspect eqidstack_webnet 验证有4个容器连接到overlay网络P+ 不停服更新/不停服扩容 手动更新服务:docker service update [opton] {some_service_name} 为{eqidstack_proxy}服务添加 [重启策略]\手动扩容:docker service scale [option] {service=replicas}将{eqidstack_proxy}服务扩容为2容器 🐽 可通过docker service inspect eqidstack_proxy验证操作结果总结docker service 定义某个(副本集)容器在生产环境下的状态,一般业务含义上的服务相关;docker stack 定义一组服务,服务间协作、调用,支撑整个业务架构;docker swarm 管理一组服务在集群节点上的的部署。
长话短说前文《解剖HttpClientFactory,自由扩展HttpMessageHandler》主要讲如何为HttpClientFactory自定义HttpMessageHandler组件本文自定义一个NLog Layout Renderer(显示HttpClient请求的耗时)什么是Layout Renderer?nlog日志上输出的特定字段,便于检索和分类。# 截取自nlog.config配置文件 xsi:type="File" layout="${date:format=yy/MM/dd HH\:mm\:ss} [${level}].[${logger}].[${threadid}}].[${elapse}]${newline}${message} ${exception:format=tostring}" fileName="${logDir}/bce-request.log" encoding="utf-8"/>以上配置输出如下日志:19/12/08 22:46:29 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[6}].[415.2504]HTTP request http://localhost:5000/v1/eqid/e741e8d600151edc000000035decf3bf after 415.2504ms end -OK 19/12/08 22:47:15 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[40}].[80.2951]HTTP request http://localhost:5000/v1/eqid/2a41e8d600151edc000000028decf3bf after 80.2951ms end -OK 19/12/08 22:48:06 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[43}].[36.8624]HTTP request http://localhost:5000/v1/eqid/1a41e8d600151edc000000028decf3bf after 36.8624ms end -OK头脑风暴nlog所有的日志Render依赖日志写入时的信息, 因此我们在写入日志时附带该Renderer值, 然后配置nlog显示日志时提取该Renderer值。1写入日志时,为Message传入参数{Url}, {Elapse}, {StatusCode}, 这三个参数值可被提取作为 Rendererpublic class CustomHttpMessageHandler : DelegatingHandler { private readonly ILogger _logger; public AttachTraceIdScopeHttpMessageHandler(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } var stopwatch = Stopwatch.StartNew(); var response = await base.SendAsync(request, cancellationToken); stopwatch.Stop(); _logger.Log(LogLevel.Information, new EventId(101, "Request End"), "HTTP request {Url} after {Elapse}ms end -{StatusCode}", request.RequestUri, stopwatch.Elapsed.TotalMilliseconds, response.StatusCode); return response; } }2添加自定义LayOutRenderer ① 简单的lambda方式, ② 我们采用稍灵活的自定义类方式:关键点是实现LayoutRenderer的抽象方法Append, 从LogEventInfo中提出Renderer值:[LayoutRenderer("elapse")] public class ElapseLayoutRenderer : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { builder.Append(logEvent.Properties["Elapse"].ToString()); } }# 参数Url、Elapse、StatusCode均可在LogEventInfo.Prpperties键值对提取3按照文档的要求,尽早注册自定义Nlog Layout Renderer:public static void Main(string[] args){ LayoutRenderer.Register<ElapseLayoutRenderer>("elapse"); ......}END关于将该HttpMessgaeHandler应用到HttpClientFactory,请参阅《解剖HttpClientFactory,自由扩展HttpMessageHandler》思路。本文演示为nlog添加自定义LayoutRenderer。
回顾《docker-compose真香》详细讲述docker-compose容器编排工具的用法,实际上容器编排yml文件在进化到版本3的时候,docker-compose更像是被定义为 适用于开发、测试环境的容器编排工具。Docker引擎在1.12版本集成了Docker Swarm,内置新的容器编排工具docker stack,① 使用方式雷同,都使用yml容器编排文件$ docker-compose -f docker-compose up$ docker stack deploy -c docker-compose.yml somestackname② 作用大体相同:这两个工具命令都能操纵docker-compose.yml文件中定义的docker services、volumes 、networks资源。现在无需另外安装docker-compose工具包, 就可以利用docker-compose.yml文件创建Docker容器堆栈。但是为什么会引入新的docker stack 容器编排技术呢?docker-compose与docker stack除了语法,还有什么不同?两者差异docker stack仅针对docker-compose版本3容器编排文件,两者对docker-compose版本3指令稍有差异化,请在这个页面中搜索"ignore"查看更多细节。 举例如下:① docker stack不支持docker-compose中的“build”指令, 相比之下docker-compose可现场构建镜像,更适合迭代开发和CIThis "build" option is ignored when deploying a stack in swarm mode with a (version 3) Compose file. The docker stack command accepts only pre-built images.② docker-compose不支持docker-compos版本3中deploy 指令,该指令定义了适用于生产部署的配置,deploy指令专属于docker stack.deployendpoint_modelabelsmodeplacementreplicasresourcesrestart_policyrollback_configupdate_configNot supported for docker stack deploydocker-compose版本2依旧有restart指令,对于生产部署来说支持不足,杯水车薪。 可以渐渐理解两者差异的趋势:- docker-compose更像是被定义为单机容器编排工具;- docker stack被定义为适用于生产环境的编排方式,强化复制集、容器重启策略、回滚策略、服务更新策略等生产特性。docker stack强化service的概念:服务可理解为发布到生产环境时某组容器的预期状态 前世docker-compose是一个Python项目,最初有一个名叫fig的Python项目能够解析fig.yml并启动docker容器堆栈, 这个工具慢慢产品化并被改名为docker-compose,但是docker-compose始终是一个Python工具,作用在Docker引擎的顶层;使用Docker API根据规范启动容器,必须单独安装docker-compose工具包才能将其与Docker一起使用。docker stack的能力来源自docker引擎原生支持,你不需要安装额外工具包就可启动docker容器堆栈(docker stack 是docker swarm的一部分)。docker stack支持与docker-compose相似的能力,但是在Docker引擎内Go语言环境中运行的,在使用docker stack命令之前你还必须创建一个swarm节点(这也不是问题)。今生docker stack, docker-compsoe两者对yml版本3文件刻意形成差异化支持。为什么docker公司要强化docker stack,因为docker stack是进阶docker swarm的必经之路;docker stack可认为是单机上的负载均衡部署,可认为是多节点集群部署(docker swarm)的特例。、画外音:希望开发者上手docker stack用于生产部署,自然过渡到docker swarm,不然跟kubernetes怎么竞争?总结docker stack、docker-compose工具都可以使用版本3编写的docker-compose.yml文件。(版本3之前的docker-compose.yml文件可继续使用docker-compose工具)如果你仅需要一个能操作多个容器的工具,依旧可以使用docker-compose工具。因为docker stack几乎能做docker-compose所有的事情,如果你打算使用docker swarm集群编排,或者生产下的容器部署,可尝试迁移到docker stack。》修改为适用于docker satck的docker-compose.yml文件,也不会花很多时间。
背景EntityFramework Core有许多新的特性,其中一个重要特性便是批量操作。批量操作意味着不需要为每次Insert/Update/Delete操作发送单独的命令,而是在一次SQL请求中发送批量组合指令。EFCore批量操作实践批处理是期待已久的功能,社区多次提出要求。现在EFCore支持开箱即用确实很棒,可以提高应用程序的性能和速度。对比实践以常见的批量插入为例,使用SQL Server Profiler观察产生并执行的SQL语句。// category表添加3条记录并执行保存 using (var c= new SampleDBContext()) c.Categories.Add(new Category() { CategoryID = 1, CategoryName = "Clothing" }); c.Categories.Add(new Category() { CategoryID = 2, CategoryName = "Footwear" }); c.Categories.Add(new Category() { CategoryID = 3, CategoryName = "Accessories" }); c.SaveChanges(); }当执行SaveChanges(), 从SQL Profiler追溯到的SQL:exec sp_executesql N'SET NOCOUNT ON;INSERT INTO [Categories] ([CategoryID], [CategoryName]) VALUES (@p0, @p1),(@p2, @p3),(@p4, @p5);',N'@p0 int,@p1 nvarchar(4000),@p2 int,@p3 nvarchar(4000),@p4 int,@p5 nvarchar(4000)', @p0=1,@p1=N'Clothing',@p2=2,@p3=N'Footwear',@p4=3,@p5=N'Accessories'如你所见,批量插入没有产生3个独立的语句,而是被组合为一个传参存储过程脚本(用列值作为参数);如果使用EF6执行相同的代码,则在SQL Server Profiler中将看到3个独立的插入语句 。下面是EFCore、EF6批量插入的对比截图:① 就性能和速度而言,EFCore批量插入更具优势② 若数据库是针对云部署,EF6运行这些查询,还将产生额外的流量成本经过验证:EFCore批量更新、批量删除功能,EFCore均发出了使用sp_executesql存储过程+批量参数构建的SQL脚本。深入分析起关键作用的存储过程sp_executesql:可以多次执行的语句或批处理 (可带参)- Syntax for SQL Server, Azure SQL Database, Azure SQL Data Warehouse, Parallel Data Warehouse sp_executesql [ @stmt = ] statement { , [ @params = ] N'@parameter_name data_type [ OUT | OUTPUT ][ ,...n ]' } { , [ @param1 = ] 'value1' [ ,...n ] } ]注意官方限制:The amount of data that can be passed by using this method is limited by the number of parameters allowed. SQL Server procedures can have, at most, 2100 parameters. Server-side logic is required to assemble these individual values into a table variable or a temporary table for processing. // SQL存储过程最多可使用2100个参数豁然开朗SqlServer sp_executesql存储过程最多支持2100个批量操作形成的列值参数,所以遇到很大数量的批量操作,EFCore SqlProvider会帮我们将批量操作分块传输,这也是我们在实际大批量使用时看到分块发送的原因。EFCore开放了【配置关系型数据库批量操作大小】:protected override void OnConfiguring(DbContextOptionsBuilder optionbuilder) string sConnString = @"Server=localhost;Database=EFSampleDB;Trusted_Connection=true;"; optionbuilder.UseSqlServer(sConnString , b => b.MaxBatchSize(1)); // 批量操作的SQL语句数量,也可设定为1禁用批量插入}总结① EFCore 相比EF6,已经支持批量操作,能有效提高应用程序的性能② EFCore的批量操作能力,由对应的DataBaseProvider支撑(Provider实现过程跟背后的存储载体密切相关);关注SQL存储过程sp_executesql,官方明文显示批量操作的列值参数最多2100个,这个关键因素决定了在大批量操作的时候 依旧会被分块传输。③ 另外一个批量操作的方法,这里也点一下:构造Rawsql 【EFCore也支持Rawsql】 sqlite不支持存储过程,为批量插入提高性能,可采用此方案:var insertStr = new StringBuilder(); insertStr.AppendLine("insert into ProfileUsageCounters (profileid,datetime,quota,usage,natureusage) values"); var txt = insertStr.AppendLine(string.Join(',', usgaeEntities.ToList().Select(x => return $"({x.ProfileId},{x.DateTime},{x.Quota},{x.Usage},{x.NatureUsage})"; }).ToArray())); await _context.Database.ExecuteSqlCommandAsync(txt.ToString());
2021年了,`IEnumerator`、`IEnumerable`接口还傻傻分不清楚?