分类 技术 下的文章

我通常会抽象地总结我为他人所做的工作(出于显而易见的原因),但是我被允许公开谈论一个网站:Vocal 。我去年为它做了一些 SRE 工作。实际上早在 2 月份,我就在 GraphQL 悉尼会议上做过一次演讲,不过这篇博客推迟了一点才发表。

Vocal 是一个基于 GraphQL 的网站,它获得了人们的关注,然后就遇到了可扩展性问题,而我是来解决这个问题的。这篇文章会讲述我的工作。显然,如果你正在扩展一个 GraphQL 站点,你会发现这篇文章很有用,但其中大部分内容讲的都是当一个站点获得了足够的流量而出现的必须解决的技术问题。如果你对站点可扩展性有兴趣,你可能想先阅读 最近我发表的一系列关于可扩展性的文章

Vocal

Vocal 是一个博客平台,内容包括日记、电影评论、文章评论、食谱、专业或业余摄影、美容和生活小贴士以及诗歌,当然,还有可爱的猫猫和狗狗照片。

Vocal 的不同之处在于,它允许人们制作观众感兴趣的作品而获得报酬。作者的页面每次被浏览都可以获得一小笔钱,还能获得其他用户的捐赠。有很多专业人士在这个平台上展示他们的工作,但对于大多数普通用户来说,他们只是把 Vocal 当作一个兴趣爱好,碰巧还能赚些零花钱作为奖励。

Vocal 是新泽西初创公司 Jerrick Media 的产品,更新:Jerrick Media 已经更名为 Creatd,在纳斯达克上市。2015 年,他们与 Thinkmill 合作一起开发,Thinkmill 是一家悉尼中型软件开发咨询公司,擅长 JavaScript、React 和 GraphQL 开发。

剧透

不幸的是,有人告诉我,由于法律原因,我不能提供具体的流量数字,但公开的信息可以说明。Alexa 对所有网站按照流量进行排名。这是我演讲中展示的 Alexa 排名图,从 2019 年 11 月到今年 2 月,Vocal 流量增长到全球排名第 5567 位。

去年 11 月到今年 2 月 Vocal 的全球排名从 9574 名增长到 5567 名

曲线增长变慢是正常的,因为它需要越来越多的流量来赢得每个位置。Vocal 现在排名 4900 名左右,显然还有很长的路要走,但对于一家初创公司来说,这一点也不寒酸。大多数初创公司都很乐意与 Vocal 互换排名。

在网站升级后不久,Creatd 开展了一项营销活动,使流量翻了一番。在技术方面,我们要做的就是观察仪表盘上的上升的数字。自发布以来的 9 个月里,只有两个平台问题需要员工干预:3 月份每五年一次的 AWS RDS 证书轮换,以及一款应用推出时遇到的 Terraform 错误。作为一名 SRE,我很高兴看到 Vocal 不需要太多的平台工作来保持运行。更新:该系统也抗过了 2020 年的美国大选,没有任何意外。

以下是本文技术内容的概述:

  • 技术和历史背景
  • 从 MongoDB 迁移到 Postgres
  • 部署基础设施的改造
  • 使应用程序兼容扩展措施
  • 让 HTTP 缓存发挥作用
  • 其他一些性能调整

一些背景信息

Thinkmill 使用 Next.js(一个基于 React 的 Web 框架)构建了一个网站,和 Keystone 在 MongoDB 前面提供的 GraphQL API 进行交互。Keystone 是一个基于 GraphQL 的无头 CMS 库:在 JavaScripy 中定义一个模式,将它与一些数据存储挂钩,并获得一个自动生成的 GraphQL API 用于数据访问。这是一个自由开源软件项目,由 Thinkmill 提供商业支持。

Vocal V2

Vocal 的第一版就受到了关注,它找到了一个喜欢它的用户群,并不断壮大,最终 Creatd 请求 Thinkmill 帮助开发 V2,并于去年 9 月成功推出。Creatd 员工避免了 第二个系统效应,他们一般都是根据用户的反馈进行改变,所以他们 主要是 UI 和功能更改,我就不赘述了。相反,我将讨论下我的工作内容:使新站点更加健壮和可扩展。

声明:我很感谢能与 Creatd 以及 Thinkmill 在 Vocal 上的合作,并且他们允许我发表这个故事,但 我仍然是一名独立顾问,我写这篇文章没有报酬,甚至没有被要求写它,这仍然是我自己的个人博客。

迁移数据库

Thinkmill 在使用 MongoDB 时遇到了几个可扩展性问题,因此决定升级到 Keystone 5 以利用其新的 Postgres 支持。

如果你从事技术工作的时间足够长,那你可能还记得 00 年代末的 “NOSQL” 营销,这可能听起来很有趣。NoSQL 的一个重要特点是,像 Postgres 这样的关系数据库(SQL)不像 MongoDB 这样“网站级规模”的 NoSQL 数据库那样具有可扩展性。从技术上将,这种说法是正确的,但 NoSQL 数据库的可扩展性来自它可以有效处理各种查询的折衷。简单的非关系数据库(如文档数据库和键值数据库)有其一席之地,但当它们用作应用的通用后端时,应用程序通常会在超出关系数据库的理论扩展限制之前,就超出了数据库的查询限制。Vocal 的原本的大多数数据库查询在 MongoDB 上运行良好,但随着时间推移,越来越多的查询需要特殊技巧才能工作。

在技术要求方面,Vocal 与维基百科非常相似。维基百科是世界上最大的网站之一,它运行在 MySQL(或者说它的分支 MariaDB)上。当然,这需要一些重要的工程来实现,但在可预见的未来,我认为关系数据库不会对 Vocal 的扩展构成严重威胁。

我做过一个比较,托管的 AWS RDS Postgres 实例的成本不到旧 MongoDB 实例的五分之一,但 Postgres 实例的 CPU 使用率仍然低于 10%,尽管它提供的流量比旧站点多。这主要是因为一些重要的查询在文档数据库架构下一直效率很低。

迁移的过程可以新写一篇博客文章来讲述,但基本上是 Thinkmill 开发人员构建了一个 ETL 管道,使用 MoSQL 来完成这项繁重的工作。由于 Keystone 对于 Postgres 支持仍然比较基础,但它是一个 FOSS 项目,所以我能够解决在 SQL 生成性能方面遇到的问题。对于这类事情,我总是推荐 Markys Winand 的 SQL 博文:使用 Luke 索引现代 SQL。他的文章很友好,甚至对那些暂时不太关注 SQL 人来说也是容易理解的,但他拥有你大多数需要的理论知识。如果你仍然有问题,一本好的、专注于即可性能的书可以帮助你。

平台

架构

V1 是几个 Node.js 应用,运行在 Cloudflare(作为 CDN)背后的单个虚拟专用服务器(VPS)上。我喜欢把避免过度工程化作为一个高度优先事项,所以我对这一架构竖起了大拇指。然而,在 V2 开始开发时,很明显,Vocal 已经超越了这个简单的架构。在处理巨大峰值流量时,它没有给 Thinkmill 开发人员提供很多选择,而且它很难在不停机情况下安全部署更新。

这是 V2 的新架构:

Vocal V2 的技术架构,请求从 CDN 进入,然后经过 AWS 的负载均衡。负载均衡将流量分配到两个应用程序 “Platform” 和 “Website”。“Platform” 是一款 Keystone 应用程序,将数据存储在 Redis 和 Postgres 中。

基本上就是两个 Node.js 应用程序复制放在负载均衡器后面,非常简单。有些人认为可扩展架构要比这复杂得多,但是我曾经在一些比 Vocal 规模大几个数量级的网站工作过,它们仍然只是在负载均衡器后面复制服务,带有 DB 后端。你仔细想想,如果平台架构需要随着站点的增长而变得越来越复杂,那么它就不是真正“可扩展的”。网站可扩展性主要是解决那些破坏可扩展的实现细节。

如果流量增长得足够多,Vocal 的架构可能需要一些补充,但它变得更加复杂的主要原因是新功能。例如,如果(出于某种原因)Vocal 将来需要处理实时地理空间数据,那将是一个与博客文章截然不同的技术,所以我预期它会进行架构上的更改。大型网站架构的复杂性主要来自于复杂的功能。

不知道未来的架构应该是什么样子很正常,所以我总是建议你尽可能从简单开始。修复一个简单架构要比复杂架构更容易,成本也更低。此外,不必要的复杂架构更有可能出现错误,而这些错误将更难调试。

顺便说一下,Vocal 分成了两个应用程序,但这并不重要。一个常见的扩展错误是,以可扩展的名义过早地将应用分割成更小的服务,但将应用分割在错误的位置,从而导致更多的可扩展性问题。作为一个单体应用,Vocal 可以扩展的很好,但它的分割做的也很好。

基础设施

Thinkmill 有一些人有使用 AWS 经验,但它主要是一个开发车间,需要一些比之前的 Vocal 部署更容易上手的东西。我最终在 AWS Fargate 上部署了新的 Vocal,这是弹性容器服务(ECS)的一个相对较新的后端。在过去,许多人希望 ECS 作为一个“托管服务运行 Docker 容器”的简单产品,但人们仍然必须构建和管理自己的服务器集群,这让人感到失望。使用 ECS Fargate,AWS 就可以管理集群了。它支持运行带有复制、健康检查、滚动更新、自动缩放和简单警报等基本功能的 Docker 容器。

一个好的替代方案是平台即服务(PaaS),比如 App Engine 或 Heroku。Thinkmill 已经在简单的项目中使用它们,但通常在其他项目中需要更大的灵活性。在 PaaS 上运行的网站规模要大得多,但 Vocal 的规模决定了使用自定义云部署是有经济意义的。

另一个明显的替代方案是 Kubernetes。Kubernetes 比 ECS Fargate 拥有更多的功能,但它的成本要高得多 —— 无论是资源开销还是维护(例如定期节点升级)所需的人员。一般来说,我不向任何没有专门 DevOps 员工的地方推荐 Kubernetes。Fargate 具有 Vocal 需要的功能,使得 Thinkmill 和 Creatd 能专心于网站改进,而不是忙碌于搭建基础设施。

另一种选择是“无服务器”功能产品,例如 AWS Lambda 或 Google 云。它们非常适合处理流量很低或很不规则的服务,但是 ECS Fargate 的自动伸缩功能足以满足 Vocal 的后端。这些产品的另一个优点是,它们允许开发人员在云环境中部署东西,但无需了解很多关于云环境的知识。权衡的结果是,无服务器产品与开发过程、测试以及调试过程紧密耦合。Thinkmill 内部已经有足够的 AWS 专业知识来管理 Fargate 的部署,任何知道如何开发 Node.js 简单的 Hello World 应用程序的开发人员都可以在 Vocal 上工作,而无需了解无服务器功能或 Fargate 的知识。

ECS Fargate 的一个明显缺点是供应商锁定。但是,避免供应商锁定是一种权衡,就像避免停机一样。如果你担心迁移,那么在平台独立性花费比迁移上更多的钱是没有意义的。在 Vocal 中,依赖于 Fargate 的代码总量小于 500 行 Terraform。最重要的是 Vocal 应用程序代码本身与平台无关,它可以在普通开发人员的机器上运行,然后打包成一个 Docker 容器,几乎可以运行在任何可以运行 Docker 容器的地方,包括 ECS Fargate。

Fargate 的另一个缺点是设置复杂。与 AWS 中的大多数东西一样,它处于一个 VPC、子网、IAM 策略世界中。幸运的是,这类东西是相对静态的(不像服务器集群一样需要维护)。

制作一个可扩展的应用程序

如果你想轻松地运行一个大规模的应用程序,需要做一大堆正确的事情。如果你遵循 应用程序设计的十二个守则 the Twelve-Factor App design ,事情会变得容易,所以我不会在这里赘述。

如果员工无法规模化操作,那么构建一个“可扩展”系统就毫无意义 —— 就像在独轮车上安装喷气式发动机一样。使 Vocal 可扩展的一个重要部分是建立 CI/CD 和 基础设施即代码 之类的东西。同样,有些部署的思路也不值得考虑,因为它们使生产与开发环境相差太大(参阅 应用程序设计守则第十守则)。生产和开发之间的任何差异都会降低应用程序的开发速度,并且(在实践中)最终可能会导致错误。

缓存

缓存是一个很大的话题 —— 我曾经做过 一个关于 HTTP 缓存的演讲,相对比较基础。我将在这里谈论缓存对于 GraphQL 的重要性。

首先,一个重要的警告:每当遇到性能问题时,你可能会想:“我可以将这个值放入缓存中吗,以便再次使用时速度更快?”微基准测试 总是 告诉你:是的。 然而,由于缓存一致性等问题,随处设置缓存往往会使整个系统 变慢。以下是我对于缓存的检查表:

  1. 是否需要通过缓存解决性能问题
  2. 再仔细想想(非缓存的性能往往更加健壮)
  3. 是否可以通过改进现有的缓存来解决问题
  4. 如果所有都失败了,那么可以考虑添加新的缓存

在 Web 系统中,你经常使用的一个缓存是 HTTP 缓存系统,因此,在添加额外缓存之前,试着使用 HTTP 缓存是一个好主意。我将在这篇文章中重点讨论这一点。

另一个常见的陷阱是使用哈希映射或应用程序内部其他东西进行缓存。它在本地开发中效果很好,但在扩展时表现糟糕。最好的办法是使用支持显式缓存库,支持 Redis 或 Memcached 这样的可插拔后端。

基础知识

HTTP 规范中有两种类型缓存:私有和公共。私有缓存不会和多个用户共享数据 —— 实际上就是用户的浏览器缓存。其余的就是公共缓存。它们包括受你控制的(例如 CDN、Varnish 或 Nginx 等服务器)和不受你控制的(代理)。代理缓存在当今的 HTTPS 世界中很少见,但一些公司网络会有。

缓存查找键通常基于 URL,因此如果你遵循“相同的内容,相同的 URL;不同的内容,不同的 URL” 规则,即,给每个页面一个规范的 URL,避免从同一个 URL 返回不同的内容这样“聪明”的技巧,缓存就会强壮一点。显然,这对 GraphQL API 端点有影响(我将在后面讨论)。

你的服务器可以采用自定义配置,但配置 HTTP 缓存的主要方法是在 Web 响应上设置 HTTP 头。最重要的标头是 cache-control。下面这一行说明所有缓存都可以缓存页面长达 3600 秒(一小时):

cache-control: max-age=3600, public

对于针对用户的页面(例如用户设置页面),重要的是使用 private 而不是 public 来告诉公共缓存不要存储响应,防止其提供给其他用户。

另一个常见的标头是 vary,它告诉缓存,响应是基于 URL 之外的一些内容而变化。(实际上,它将 HTTP 头和 URL 一起添加到缓存键中。)这是一个非常生硬的工具,这就是为什么尽可能使用良好 URL 结构的原因,但一个重要的用例是告诉浏览器响应取决于登录的 cookie,以便在登录或注销时更新页面。

vary: cookie

如果页面根据登录状态而变化,你需要 cache-control:private(和 vary:cookie),即使是在公开的、已登出的页面版本上,以确保响应不会混淆。

其他有用的标头包括 etaglast-modified,但我不会在这里介绍它们。你可能仍然会看到一些诸如 expirespragma:cache 这种老旧的 HTTP 标头。它们早在 1997 年就被 HTTP/1.1 淘汰了,所以我只在我想禁用缓存或者我感到偏执时才使用它们。

客户端标头

鲜为人知的是,HTTP 规范允许在客户端请求中使用 cache-control 标头以减少缓存时间并获得最新响应。不幸的是,似乎大多数浏览器都不支持大于 0 的 max-age ,但如果你有时在更新后需要一个最新响应,no-cache 会很有用。

HTTP 缓存和 GraphQL

如上,正常的缓存键是 URL。但是 GraphQL API 通常只使用一个端点(比如说 /api/)。如果你希望 GraphQL 查询可以缓存,你需要查询参数出现在 URL 路径中,例如 /api/?query={user{id}}&variables={"x":99}(忽略了 URL 转义)。诀窍是将 GraphQL 客户端配置为使用 HTTP GET 请求进行查询(例如,apollo-link-http 设置为 useGETForQueries )。

Mutation 不能缓存,所以它们仍然需要使用 HTTP POST 请求。对于 POST 请求,缓存只会看到 /api/ 作为 URL 路径,但缓存会直接拒绝缓存 POST 请求。请记住,GET 用于非 Mutation 查询(即幂等),POST 用于 Mutation(即非幂等)。在一种情况下,你可能希望避免使用 GET 查询:如果查询变量包含敏感信息。URL 经常出现在日志文件、浏览器历史记录和聊天中,因此 URL 中包含敏感信息通常是一个坏主意。无论如何,像身份验证这种事情应该作为不可缓存的 Mutation 来完成,这是一个特殊的情况,值得记住。

不幸的是,有一个问题:GraphQL 查询往往比 REST API URL 大得多。如果你简单地切换到基于 GET 的查询,你会得到一些非常长的 URL,很容易超过 2000 字节的限制,目前一些流行的浏览器和服务器还不会接受它们。一种解决方案是发送某种查询 ID,而不是发送整个查询,即类似于 /api/?queryId=42&variables={"x":99}。Apollo GraphQL 服务器对此支持两种方式:

一种方法是 从代码中提取所有 GraphQL 查询,并构建一个服务器端和客户端共享的查询表。缺点之一是这会使构建过程更加复杂,另一个缺点是它将客户端项目与服务器项目耦合,这与 GraphQL 的一个主要卖点背道而驰。还有一个缺点是 X 版本和 Y 版本的代码可能识别一组不同的查询,这会成为一个问题,因为 1:复制的应用程序将在更新推出或回滚期间提供多个版本,2:客户端可能会使用缓存的 JavaScript,即使你升级或降级服务器。

另一种方式是 Apollo GraphQL 所宣称的 自动持久查询(APQ)。对于 APQ 而言,查询 ID 是查询的哈希值。客户端向服务器发出请求,通过哈希查询。如果服务器无法识别该查询,则客户端会在 POST 请求中发送完整的查询,服务器会保存此次查询的散列值,以便下次识别。

HTTP 缓存和 Keystone 5

如上所述,Vocal 使用 Keystone 5 生成 GraphQL API,而 Keystone 5 和 Apollo GraphQL 服务器配合一起工作。那么我们是如何设置缓存标头的呢?

Apollo 支持 GraphQL 模式的 缓存提示 cache hint 。巧妙地是,Apollo 会收集查询涉及的所有内容的所有缓存提示,然后它会自动计算适当的缓存标头值。例如,以这个查询为例:

query userAvatarUrl {
    authenticatedUser {
        name
        avatar_url
    }
}

如果 name 的最长期限为 1 天,而 avatar_url 的最长期限为 1 小时,则整体缓存最长期限将是最小值,即 1 小时。authenticatedUser 取决于登录 cookie,因此它需要一个 private 提示,它会覆盖其他字段的 public,因此生成的 HTTP 头将是 cache-control:max-age=3600,private

对 Keystone 列表和字段添加了缓存提示支持。以下是一个简单例子,在文档的待办列表演示中给一个字段添加缓存提示:

const keystone = new Keystone({
  name: 'Keystone To-Do List',
  adapter: new MongooseAdapter(),
});

keystone.createList('Todo', {
  schemaDoc: 'A list of things which need to be done',
  fields: {
    name: {
      type: Text,
      schemaDoc: 'This is the thing you need to do',
      isRequired: true,
      cacheHint: {
        scope: 'PUBLIC',
        maxAge: 3600,
      },
    },
  },
});

另一个问题:CORS

令人沮丧的是, 跨域资源共享 Cross-Origin Resource Sharing (CORS)规则会与基于 API 网站中的缓存产生冲突。

在深入问题细节之前,让我们跳到最简单的解决方案:将主站点和 API 放在一个域名上。如果你的站点和 API 位于同一个域名上,就不必担心 CORS 规则(但你可能需要考虑 限制 cookie)。如果你的 API 专门用于网站,这是最简单的解决方案,你可以愉快地跳过这一节。

在 Vocal V1 中,网站(Next.js)和平台(Keystone GraphQL)应用程序处于不同域(vocal.mediaapi.vocal.media)。为了保护用户免受恶意网站的侵害,现代浏览器不会随便让一个网站与另一个网站进行交互。因此,在允许 vocal.mediaapi.vocal.media 发出请求之前,浏览器会对 api.vocal.media 进行“预检”。这是一个使用 OPTIONS 方法的 HTTP 请求,主要是询问跨域资源共享是否允许。预检通过后,浏览器会发出最初的正常请求。

令人沮丧的是,预检是针对每个 URL 的。浏览器为每个 URL 发出一个新的 OPTIONS 请求,服务器每次都会响应。服务器没法说 vocal.media 是所有 api.vocal.media 请求的可信来源 。当所有内容都是对一个 API 端点的 POST 请求时,这个问题并不严重,但是在为每个查询提供 GET 式 URL 后,每个查询都因预检而延迟。更令人沮丧的是,HTTP 规范说 OPTIONS 请求不能被缓存,所以你会发现你所有的 GraphQL 数据都被完美地缓存在用户身旁的 CDN 中,但浏览器仍然必须向源服务器发出所有的预检请求。

如果你不能只使用一个共享的域,有几种解决方案。

如果你的 API 足够简单,你或许可以利用 CORS 规则的例外

某些缓存服务器可以配置为忽略 HTTP 规范,任何情况都会缓存 OPTIONS 请求。例如,基于 Varnish 的缓存和 AWS CloudFrone。这不如完全避免预检那么有效,但比默认的要好。

另一个很魔改的选项是 JSONP。当心:如果做错了,那么可能会创建安全漏洞。

让 Vocal 更好地利用缓存

让 HTTP 缓存在底层工作之后,我需要让应用程序更好地利用它。

HTTP 缓存的一个限制是它在响应级别上要么是全有要么是全无的。大多数响应都可以缓存,但如果一个字节不能缓存,那整个页面就无法缓存。作为一个博客平台,大多数 Vocal 数据都是可缓存的,但在旧网站上,由于右上角的菜单栏,几乎没有页面可以缓存。对于匿名用户,菜单栏将显示邀请用户登录或创建账号的链接。对于已登录用户,它会变成用户头像和用户个人资料菜单,因为页面会根据用户登录状态而变化,所以无法在 CDN 中缓存任何页面。

Vocal 的一个典型页面。该页面的大部分内容都是高度可缓存的内容,但在旧网站中,由于右上角有一个小菜单,实际上没有一个内容是可缓存的。

这些页面是由 React 组件的服务器端渲染(SSR)的。解决方法是将所有依赖于登录 cookie 的 React 组件,强制它们 只在客户端进行延迟呈现。现在,服务器会返回完全通用的页面,其中包含用于个性化组件(如登录菜单栏)的占位符。当页面在浏览器中加载时,这些占位符将通过调用 GraphQL API 在客户端填充。通用页面可以安全地缓存到 CDN 中。

这一技巧不仅提高了缓存命中率,还帮助改善了人们感知的页面加载时间。空白屏幕和旋转动画让我们不耐烦,但一旦第一个内容出现,它会分散我们几百毫秒的注意力。如果人们在社交媒体上点击一个 Vocal 帖子的链接,主要内容就会立即从 CDN 中出现,很少有人会注意到,有些组件直到几百毫秒后才会完全出现。

顺便说一下,另一个让用户更快地看到第一个内容的技巧是 流式渲染,而不是等待整个页面渲染完成后再发送。不幸的是,Node.js 还不支持这个功能

拆分响应来提高可缓存性也适用于 GraphQL。通过一个请求查询多个数据片段的能力通常是 GraphQL 的一个优势,但如果响应的不同部分具有差别很大的缓存,那么最好将它们分开。举个简单的例子,Vocal 的分页组件需要知道当前页面的页数和内容。最初,组件在一个查询中同时获取两个页面,但由于页面的总数是所有页面的一个常量,所有我将其设置为一个单独的查询,以便缓存它。

缓存的好处

缓存的明显好处是它减轻了 Vocal 后端服务器的负载。这很好。但是依赖缓存来获得容量是危险的,你仍然需要一个备份计划,以便当有一天你不可避免地放弃缓存。

提高页面响应速度是使用缓存是一个更好的理由。

其他一些好处可能不那么明显。峰值流量往往是高度本地化的。如果一个有很多社交媒体粉丝的人分享了一个页面的链接,那么 Vocal 的流量就会大幅上升,但主要是指向分享的那个页面及其元素。这就是为什么缓存擅长吸收最糟糕的流量峰值,它使后端流量模式相对更平滑,更容易被自动伸缩处理。

另一个好处是 优雅的退化 graceful degradation 。即使后端由于某些原因出现了严重的问题,站点最受欢迎的部分仍然可以通过 CDN 缓存来提供服务。

其他的性能调整

正如我常说的,可扩展的秘诀不是让事情变得更复杂。它只是让事情变得不比需要的更复杂,然后彻底解决所有阻碍扩展的东西。扩展 Vocal 的规模涉及到许多小事,在这篇文章中无法一一说明。

一个经验:对于分布式系统中难以调试的问题,最困难的部分通常是获取正确的数据,从而了解发生的原因。我能想到很多时候,我被困住了,只能靠猜测来“即兴发挥”,而不是想办法找到正确的数据。有时这行得通,但对复杂的问题却不行。

一个相关技巧是,你可以通过获取系统中每个组件的实时数据(甚至只是 tail -F 的日志),在不同的窗口中显示,然后在另一个窗口中单击网站来了解很多信息。比如:“为什么切换这个复选框会在后台产生这么多的 DB 查询?”

这里有个修复的例子。有些页面需要几秒钟以上的时间来呈现,但这个情况只会在部署环境中使用 SSR 时会出现。监控仪表盘没有显示任何 CPU 使用量峰值,应用程序也没有使用磁盘,所以这表明应用程序可能正在等待网络请求,可能是对后端的请求。在开发环境中,我可以使用 sysstat 工具来记录 CPU、RAM、磁盘使用情况,以及 Postgres 语句日志和正常的应用日志来观察应用程序是如何工作的。Node.js 支持探测跟踪 HTTP 请求,比如使用 bpftrace,但处于某些无聊的原因,它们不能在开发环境中工作,所以我在源代码中找到了探针,并创建了一个带有请求日志的 Node.js 版本。我使用 tcpdump 记录网络数据,这让我找到了问题所在:对于网站发出的每一个 API 请求,都要创建一个新的网络连接到 “Platform”。(如果这都不起作用,我想我会在应用程序中添加请求跟踪功能。)

网络连接在本地机器上速度很快,但在现实网络上却不可忽略。建立加密连接(比在生产环境中)需要更长时间。如果你向一个服务器(比如一个 API)发出大量请求,保持连接打开并重用它是很重要的。浏览器会自动这么做,但 Node.js 默认不会,因为它不知道你是否发出了很多请求,所以这个问题只出现在 SSR 上。与许多漫长的调试过程一样,修复却非常简单:只需将 SSR 配置为 保持连接存活,这样会使页面的呈现时间大幅下降。

如果你想了解更多这方面的知识,我强烈建议你阅读《高性能浏览器网络》这本书(可免费在线阅读),并跟进 Brendan Gregg 发表的指南

你的站点呢?

实际上,我们还可以做很多事情来提升 Vocal 的速度,但我们没有全做。这是因为在初创公司和在大公司身为一个固定员工做 SRE 工作还是有很大区别的。我们的目标、预算和发布日期都很紧张,但最终我们的网站得到了很大改善,给了用户他们想要的东西。

同样的,你的站点有它自己的目标,并且可能与 Vocal 有很大的不同。然而,我希望这篇文章和它的链接至少能给你一些有用的思路,为用户创造更好的东西。


via: https://theartofmachinery.com/2020/06/29/scaling_a_graphql_site.html

作者:Simon Arneaud 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

链接一直是 UNIX 文件系统的一个独特的高级功能。

 title=

UNIX 和 Linux 用户发现链接有很多用途,特别是符号链接。我喜欢使用符号链接的一种方式是管理各种 IT 设备的配置备份。

我有一个目录结构,用来存放我的文档、更新及网络上其他和计算机和设备有关的文件。设备可以包括路由器、接入点、NAS 服务器和笔记本电脑,通常有不同的品牌和版本。配置备份本身可能在目录树的深处,例如 /home/alan/Documents/network/device/NetgearRL5000/config

为了简化备份过程,我在主目录中有一个名为 Configuration 的目录。我使用这个目录的符号链接来指向特定的设备目录:

:~/Configuration/ $ ls -F1
Router@
Accesspoint@
NAS@

注意ls 命令的 -F 选项在每个文件名上附加特殊字符以表示其类型。如上所示,@ 符号表示这些是链接。

创建一个链接

符号链接 Router 指向我的 Netgear RL5000 的 config 目录。创建它的命令是 ln -s

$ ln -s /home/alan/Documents/network/device/NetgearRL5000/config Router

然后,用 ls -l 看一下并确认:

:~/Configuration/ $ ls -l
Router -> /home/alan/Documents/network/device/NetgearRL5000/config
NAS -> /home/alan/Documents/network/device/NFSBox/config
...

这样做的好处是,当对这个设备进行维护时,我只需进入 ~/Configuration/Router

如果我决定用一个新的型号替换这个路由器,使用符号链接的第二个好处就很明显了。我可能会把旧的路由器改成一个接入点。因此,它的目录并没有被删除。相反,我有一个新的目录,对应于新的路由器,也许是华硕 DF-3760。我创建这个目录并确认它的存在:

$ mkdir -p ~/Documents/network/device/ASUSDF-3760/config
:~/Documents/network/device/ $ ls
NetgearRL5000
ASUSDF-3760
NFSBox
...

另一个例子是,如果你的办公室里有几个接入点。你可以使用符号链接在逻辑上代表每一个,用一个通用的名字,如 ap1ap2,等等,或者你可以使用描述性的词语,如 ap_floor2ap_floor3,等等。这样,当物理设备随时间变化时,你不必持续更新任何可能管理它们的进程,因为它们是在处理链接而不是实际的设备目录。

更新一个链接

由于我的主路由器已经改变,我想让路由器的符号链接指向它的目录。我可以使用 rmln 命令来删除和创建一个新的符号链接,但是有一种方法可以只用 ln 命令和几个选项就可以一步完成:

:~/Configuration/ $ ln -vfns ~/Documents/network/device/ASUSDF-3760/config/ Router
'Router' -> '/home/alan/Documents/network/device/ASUSDF-3760/config/'
:~/Configuration/ $ ls -l
Router -> /home/alan/Documents/network/device/ASUSDF-3760/config
NAS -> /home/alan/Documents/network/device/NFSBox/config

根据手册页,这些选项如下:

  • -v--verbose:打印每个链接文件的名称
  • -f--force:删除目标文件(有必要,因为已经存在一个链接)
  • -n--no-dereference:如果链接名是一个目录的符号链接,就把它当作一个正常的文件
  • -s--symbolic:制作符号链接而不是硬链接

总结

链接是 UNIX 和 Linux 文件系统中最强大的功能之一。其他操作系统也曾试图模仿这种能力,但由于他们的文件系统缺乏基本的链接设计,这些系统从来没有工作得那么好,也没有那么可用。

上面的演示只是利用链接在生活生产环境中无缝浏览不断变化的目录结构的众多可能性中的一种。链接提供了一个永远不会长期静态的组织所需的灵活性。


via: https://opensource.com/article/21/11/update-linux-file-system-link

作者:Alan Formy-Duval 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

用这个有趣的 Linux 命令行工具来庆祝节日吧。

 title=

你可能听说过这样一个小程序:它能接受输入信息(比如你通过键盘输入的消息),并输出一张引用了输入消息的牛的图像。这个小程序被称为 cowsay,之前我们已经 介绍 过了。

所以,为了搞点有趣的事,我想用它来庆祝 亡灵节 Día de los Muertos (LCTT 译注:墨西哥传统的鬼节,著名动画电影《 寻梦环游记 Coco 》即以此为背景)。

除了牛之外,其实还有一些其他的可用图像。当安装 cowsay 时,程序会自动安装其他几个图像,并存储在 /user/share/cowsay 目录中。你可以用 -l 参数来获取图像列表。

$ sudo dnf install cowsay
$ cowsay -l

实际上还有很多与 cowsay 或类似程序相关的开发活动。你可以创建自己的图像文件,也可以下载其他人制作的图像。例如,GitHub 上就有 Charc0al 的 cowsay 文件转换器。你可以用这一工具将自己的图片转换为 cowsay 所需的特殊 ASCII 格式文件。根据 Linux 或 FreeBSD 终端设置不同,你可能会启用颜色支持,而 cowsay 也可以显示彩色图像。Charc0al 的转换器也提供了许多现成的颜色文件。

我选择了“ 甲壳虫汁 Beetlejuice ”(LCTT 译注:同名美国奇幻喜剧电影中的主角大法师)文件来开展我的“庆祝活动”。首先,我将 beetlejuice.cow 文件保存到了 /usr/share/cowsay 目录。这个目录权限属于 root 用户,你可以先将该文件保存到家目录,然后再复制过去。此外我们还需要将该文件的读取权限赋予所有用户。

$ sudo cp beetlejuice.cow /usr/share/cowsay
$ sudo chmod o+r /usr/share/cowsay/beetlejuice.cow

关注一下图像是如何生成的(过程很有趣)。首先将各种 ASCII 颜色控制代码设置为变量,然后用这些变量,以传统的 ASCII 艺术风格绘制图像。生成的图像几乎是全身的,并且在不滚动屏幕的情况下,不适配我的终端的高度,所以我编辑了一下该文件,删除了最后 15 行以降低高度。

这个图像也可以被 cowsay 程序检测到,并展示在列表中。

$ cowsay -l
Cow files in /usr/share/cowsay:
beavis.zen beetlejuice blowfish bud-frogs bunny cheese cower default dragon
...

现在,只要运行程序,并使用 -f 选项指定该图像就可以了。别忘了提供要输出的信息。

$ cowsay -f beetlejuice "Happy Day of the Dead!"

ASCII display of Beetlejuice via cowsay

“甲壳虫汁”祝你亡灵节快乐 (CC BY-SA 4.0)

cowsay 是 Linux 中一个有趣的搞怪小玩意。发挥你的创意,探索一下 cowsay 以及 ASCII 的艺术吧。


via: https://opensource.com/article/21/11/linux-cowsay

作者:Alan Formy-Duval 选题:lujun9972 译者:unigeorge 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我敢打赌你使用过 Linux 上的 ls 命令,它是你 学习 Linux 时首次接触到的命令之一。

这个简单的 ls 命令列出目录的内容十分方便,但是直到我发现 exa 之前从来没想过会有命令能替代它。

exa 命令简介

exa 是一个命令行工具,可以列出指定路径(如未指定则是当前目录)的目录和文件。这也许听起来很熟悉,因为这就是 ls 命令所做的事情。

exa 被视作从 UNIX 旧时代延续至今的古老的 ls 命令的一个现代替代品。如其所声称的那样,它有比 ls 命令更多的功能、更好的默认行为。

exa 功能

以下是一些你应该使用 exa 替代 ls 的原因:

  • exals 一样可移植(在所有主流 Linux 发行版、*BSD 和 macOS 上可用)
  • 默认彩色输出
  • exa 不同格式化的“详细”输出也许会吸引 Linux/BSD 新手
  • 文件查询是并行进行的,这使得 exals 的性能相当
  • 显示单个文件的 git 暂存或未暂存状态

exa 的另外一个不同的地方是它是用 Rust 编写的。顺便说一句,Rust 与 C 语言的执行速度相近,但在编译时减少了内存错误,使你的软件可以快速而安全地执行。

在 Linux 系统上安装 exa

exa 最近很流行,因为许多发行版开始将其包括在其官方软件库中。也就是说,你应该可以使用你的 [发行版的包管理器] 来安装它。

从 Ubuntu 20.10 开始,你可以使用 apt 命令来安装它:

sudo apt install exa

Arch Linux 已经有了它,你只需要 使用 pacman 命令 即可:

sudo pacman -S exa

如果它无法通过你的包管理器安装,请不要担心。毕竟它是一个 Rust 包,你可以很容易地用 Cargo 安装它。请确保在你使用的任何发行版 或 Ubuntu 上安装了 Rust 和 Cargo

安装 Rust 和 Cargo 后,使用此命令安装 exa

cargo install exa

使用 exa

exa 有很多命令选项,主要是为了更好的格式化输出和一些提高舒适度的改进,比如文件的 git 暂存或未暂存状态等等。

下面是一些屏幕截图,展示了 exa 是如何在你的系统上工作的。

简单地使用 exa 命令将产生类似于 ls 但带有颜色的输出。这种彩色的东西可能没有那么吸引人,因为像 Ubuntu 这样的发行版至少在桌面版本中已经提供了彩色的 ls 输出。不过,ls 命令本身默认没有彩色输出。

exa

exa 命令的输出截图,没有任何额外的标志

请注意,exals 命令的选项不尽相同。例如,虽然 -l 选项在 exals 中都给出了长列表,但 -h 选项添加了一个列标题,而不是 ls 的人类可读选项。

exa -lh

正如我之前提到的,exa 有列标题以获得更好的“详细”输出

我前面说过,exa 已经内置了 Git 集成。下面的屏幕截图给出了 –git 标志的演示。请注意 test_filegittracked 列中显示 -N ,因为它尚未添加到存储库中。

exa --git -lh

演示 git 标志如何与 exa 一起工作

下面的例子不是我的猫键入的。它是各种选项的组合。exa 有可供你尝试和探索的很多选项。

exa -abghHliS

一个非常丰富多彩和详细的输出,具有用户友好的详细输出

你可以通过在终端中运行以下命令来获取完整的选项列表:

exa --help

但是,如果你想了解 exa 所提供的功能,可以查看其 Git 存储库 上的 官方文档

值得从 ls 切换到 exa 吗?

对于类 UNIX 操作系统的新手来说,exa 可能是用户友好的,它牺牲了在脚本中容易使用的能力,以换取“易用性”和外观。其中,显示得更清楚并不是一件坏事。

无论如何,ls 就像通用命令。你可以将 exa 用于个人用途,但在编写脚本时,请坚持使用 ls。当预期输出与任一命令中的实际输出不匹配时,lsexa 之间一个 [或多个] 标志的差异可能会让你发疯。

我想知道你对 exa 的看法。你已经尝试过了吗?你对它的体验如何?


via: https://itsfoss.com/exa/

作者:Pratham Patel 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

FreeDOS 下批处理文件的实用指南。

 title=

即使你以前没有使用过 DOS,你也可能知道它的命令行 shell,即 COMMAND.COM。它已经成为 DOS 的同义词,FreeDOS 为此也实现了一个类似的 shell,称为 “FreeCOM”,但也命名为 COMMAND.COM,就像在其他 DOS 系统上一样。

但是 FreeCOM shell 可以做的不仅仅是为你提供一个命令行提示符让你在其中运行命令,如果你需要在 FreeDOS 上自动执行任务,你可以使用 批处理文件,也称为 “BAT 文件”,因为这些脚本使用 .BAT 扩展名。

批处理文件可能比你在 Linux 编写的脚本要简单得多。因为在很久以前,这个功能最初被添加到 DOS 时,它是为了让 DOS 用户“批量处理”某些命令。它的条件分支没有太大的灵活性,也不支持更高级的功能,例如算术扩展、标准输出和错误消息的重定向、后台进程、测试、循环(这项支持)和 Linux 脚本中常见的其他结构。

本文是 FreeDOS 下批处理文件的实用指南。记住通过用百分号(%)包裹变量名称来引用环境变量,例如 %PATH%。但是,请注意,由于历史原因,FOR 循环的构造略有不同。

打印输出

批处理文件可能需要向用户打印消息,让用户知道发生了什么。使用 ECHO 语句打印消息。例如,一个批处理文件可能使用以下语句表明它已完成了任务:

ECHO Done

ECHO 语句不需要引号。FreeCOM ECHO 语句不会以任何特殊方式处理引号,它会像普通文本一样打印它们。

通常,FreeDOS 在执行批处理文件时会打印每一行。这在一个非常短的批处理文件中通常不是问题,它只为用户定义了几个环境变量。但是对于执行更多工作的较长批处理文件而言,批处理行的这种一直显示可能会变得很麻烦。要阻止此输出,在 ECHO 语句中使用 OFF 关键字,如下所示:

ECHO OFF

使用 ON 关键字在 FreeDOS 运行时恢复显示批处理行。

ECHO ON

大多数批处理文件在第一行包含一个 ECHO OFF 语句,以阻止消息,但是 shell 在执行语句时仍然会在屏幕上打印 ECHO OFF。为了隐藏该语句,批处理文件通常在前面使用 @ 符号。这样,任何以这个特殊字符开头的行都不会打印,即使打开了 ECHO

@ECHO OFF

注释

编写较长批处理文件时,大多数程序员都喜欢使用 注释 来提醒自己这个批处理文件的用途。在批处理文件中注释,使用 REM(remark)关键字。REM 之后的任何内容都会被 FreeCOM shell 忽略。

@ECHO OFF
REM This is a comment

执行“辅助”批处理文件

通常,FreeCOM 一次只运行一个批处理文件。但是,你可能需要使用另一个批处理文件来执行其他操作,例如为多个批处理文件设置公共环境变量。

如果你从"正在运行"的批处理文件中直接调用第二个批处理文件,FreeCOM 将完全切换到第二个批处理文件,并停止处理第一个。要改为在第一个批处理文件“内部”运行第二个批处理文件,你需要告诉 FreeDOS shell 使用 CALL 关键字去 调用 第二个批处理文件。

@ECHO OFF
CALL SETENV.BAT

条件分支

批处理文件确实支持使用 IF 语句的简单条件分支。它有三种基本形式:

  1. 测试上一条命令的返回状态
  2. 测试一个变量是否等于一个值
  3. 测试文件是否存在

IF 语句的一个常见用途是测试程序是否成功返回。如果它们正常运行,大多数程序将返回零值,或者在出现错误时返回一些其他值。在 DOS 中,这称为 错误级别,这是 IF 测试的特例。

测试名为 MYPROG 的程序是否成功退出,实际上是检查程序是否返回“零”。使用 ERRORLEVEL 关键字来测试特定值。例如:

@ECHO OFF
MYPROG
IF ERRORLEVEL 0 ECHO Success

使用 ERRORLEVEL 测试错误级别是检查程序退出状态的笨拙方法。检查 DOS 程序的不同返回值,更有用的方法是使用 FreeDOS 为你定义的特殊变量,称为 ERRORLEVEL。它存储了最近执行程序的错误级别,然后你可以使用 == 测试不同的值。

你可以使用 ==IF 语句来测试变量是否等于某个值。就像一些编程语言,你可以使用 == 直接比较两个值。通常,在一侧引用一个环境变量,在另一侧引用一个值,但你也可以比较两个变量的值以查看它们是否相同。例如,你可以使用此批处理文件重写上面的 ERRORLEVEL 代码:

@ECHO OFF
MYPROG
IF %ERRORLEVEL%==0 ECHO Success

IF 语句的另一个常见用途是测试文件是否存在,如果存在则采取操作。你可以使用 EXIST 关键字来测试。例如,要删除名为 TEMP.DAT 的临时文件,你可以在批处理文件中使用以下行:

@ECHO OFF
IF EXIST TEMP.DAT DEL TEMP.DAT

对于任何 IF 语句,你都可以使用 NOT 关键字来 否定 测试。在文件 存在时打印消息,你可以这样写:

@ECHO OFF
IF NOT EXIST TEMP.DAT ECHO No file

分支执行

利用 IF 测试的一种方法是跳转到批处理文件中完全不同的部分,这取决于 IF 测试的结果。在最简单的情况下,如果一个关键命令失败,你可能希望跳到批处理文件的末尾。或者,如果某些环境变量设置不正确,你可能想要执行其他语句。

你可以使用 GOTO 指令跳转到批处理文件的其他部分。它会跳转到批处理文件中称为 标签 的特定行。注意,这是一个严格的 “go-to” 跳转:批处理文件执行将在新标签处启动。

假设程序需要一个现有的空文件来存储临时数据,如果文件不存在,则需要在运行程序之前创建一个文件。你可以将这些动作添加到批处理文件中,这样你的程序始终有一个临时文件可供使用:

@ECHO OFF
IF EXIST temp.dat GOTO prog
ECHO Creating temp file...
TOUCH temp.dat
:prog
ECHO Running the program...
MYPROG

当然,这是一个非常简单的例子。对于这种情况,你可以重写批处理文件,将创建临时文件作为 IF 语句的一部分:

@ECHO OFF
IF NOT EXIST temp.dat TOUCH temp.dat
ECHO Running the program...
MYPROG

迭代

如果你需要对一组文件执行相同的任务怎么办?你可以使用 FOR 循环 迭代 一组文件。这是一个单行循环,每次使用不同的文件运行单个命令。

FOR 循环对迭代变量使用一种特殊的语法,它的用法与其他 DOS 环境变量不同。要循环编辑一组文本文件,可以使用以下语句:(LCTT 译注:原文此处写错了,少写了一个 %

@ECHO OFF
FOR %%F IN (*.TXT) DO EDIT %%F

注意,如果在命令行中运行此循环,而不是在批处理文件中,那么迭代变量仅需要指定一个百分号(%):

C:\> FOR %F IN (*.TXT) DO EDIT %F

命令行处理

在运行批处理文件时,FreeDOS 提供了一种简单的方法来检测用户可能提供的命令行选项。FreeDOS 解析命令行输入,并将前九个选项存储在特殊变量 %1%2 ..... 等中,直到 %9。注意,无法通过这种方式直接访问第十一个(及之后)选项。特殊变量 %0 存储批处理文件的名称。

如果你的批处理文件需要处理 9 个以上的选项,你可以使用 SHIFT 语句移除第一个选项,并将每个选项向下 移动 一个值。所以第二个选项变成了 %1,第十个选项变成了 %9

大多数批处理文件只需要移动一个值。但是,如果你需要以其他增量进行移位,可以将参数提供给 SHIFT 语句。例如:

SHIFT 2

下面是一个简单的批处理文件,演示了移位操作:

@ECHO OFF
ECHO %1 %2 %3 %4 %5 %6 %7 %8 %9
ECHO Shift by one ..
SHIFT 1
ECHO %1 %2 %3 %4 %5 %6 %7 %8 %9

执行带有十个选项的批处理文件显示了 SHIFT 语句如何重新排列命令行选项,因此批处理文件现在可以用 %9 访问第十个参数:

C:\SRC>args 1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9
Shift by one ..
2 3 4 5 6 7 8 9 10
C:\SRC>

via: https://opensource.com/article/21/6/automate-tasks-bat-files-freedos

作者:Jim Hall 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这里有一些我最喜欢的 ImageMagick 技巧,以及如何在没有 GUI 的情况下使用它们。

Linux 对摄影师和图形艺术家很有用。它提供了许多工具来编辑包括照片在内的不同类型的图像文件和格式。这表明你甚至不需要一个图形界面来处理你的照片。这里有四种你可以在命令行中编辑图像的方法。

给你的图片应用效果

几年前,Seth Kenlon 写过一篇文章,4 个有趣的(半无用的)Linux 玩具,其中包括对 ImageMagick 编辑工具套件的介绍。在 2021 年的今天,ImageMagick 甚至更有意义。

这篇文章让我们了解了 Fred 的 ImageMagick 脚本,这些脚本真的很有用。Fred Weinhaus 维护着 200 多个脚本,用于对你的图像文件应用各种效果。Seth 向我们展示了 Fred 的 vintage3 脚本的一个例子,该脚本使图像变得怀旧。

创建照片拼贴画

今年,Jim Hall 用他的文章 从 Linux 命令行创建照片拼贴 向我们展示了如何从照片中创建拼贴画。

拼贴画在小册子和手册中使用得很多。它们是一种在一张照片中展示几张图片的有趣方式。可以应用效果来将它们进一步融合在一起。事实上,我以他的文章为指导,创造了上面的图片拼贴。这是我小时候的样子。以下是我使用的命令:

$ montage Screenshot-20211021114012.png \
  Screenshot-20211021114220.png \
  Screenshot-20211021114257.png \
  Screenshot-20211021114530.png \
  Screenshot-20211021114639.png \
  Screenshot-20211021120156.png \
  -tile 3x2 -background black \
  screenshot-montage.png

调整图像大小

Jim 发表了另一篇文章,从 Linux 终端调整图像的大小。这个教程演示了如何使用 ImageMagick 改变一个图像文件的尺寸并将其保存为一个新的文件。例如,上面的 montage 命令所产生的拼贴画没有达到要求的尺寸。学习如何调整尺寸,使我能够调整宽度和高度,从而使它能够被包括在内。这是我用来调整这张图片大小的命令。

 title=

$ convert screenshot-montage.png -resize 520x292\! alanfd-kid-montage.png

自动化图像处理

最近,我决定自己看一下 ImageMagick 套件。这一次,我把它的工具组合成一个 Bash 脚本。文章的题目是 用这个 bash 脚本自动处理图像。这个例子是一个简单的脚本,可以自动为我的文章制作图片。它是根据 Opensource.com 上的要求定制的。如果你想使用这个脚本,我在文章中提供了一个 Git 仓库连接。它很容易修改和扩展,可以满足任何人的需要。

总结

我希望你喜欢这些文章并在你的艺术创作中使用 Linux。如果你想看看更多的 Linux 图像软件,可以看看 Fedora Design Suite Spin。它是一个完整的操作系统,包括许多不同的开源多媒体制作和发布工具,例如:

  • GIMP
  • Inkscape
  • Blender
  • Darktable
  • Krita
  • Scribus
  • 等等

via: https://opensource.com/article/21/11/edit-photos-linux-command-line

作者:Alan Formy-Duval 选题:lujun9972 译者:geekpi 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出