Month: 9月 2020

Nextjs 中遇到的一些坑

nextjs 的 Link 无法自定义 escape

nextjs 中的 Link 的 href 对象如果传的是字典,直接调用的是 nodejs 的 URL 库,不能自定义 escape, 比如说空格会被强制格式化成加好,而不是 %20. 而且好像它使用的这个 API 在 11.0 已经 deprecated 了,所以需要啥 url 的话,还是自己格式化吧~

不支持 loading spinner

Nextjs 不支持在页面跳转的时候触发 Loading Spinner, 也就是转动的小圆圈,所以需要自己实现一下,可以用 nprogress

在 _app.js 中:

import Router from 'next/router';
import NProgress from 'nprogress'; //nprogress module
import 'nprogress/nprogress.css'; //styles of nprogress

//Binding events. 
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());

function MyApp({ Component, pageProps }) {
    return <Component {...pageProps} />
}
export default MyApp;

代理后端 API 服务器

在 next.config.js 中配置重定向:

module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/proxy/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
      },
    ]
  },
}

参考

  1. https://levelup.gitconnected.com/improve-ux-of-your-next-js-app-in-3-minutes-with-page-loading-indicator-3a422113304d
  2. https://github.com/vercel/next.js/discussions/14057
  3. https://nextjs.org/docs/api-reference/next.config.js/rewrites

React Hooks

使用 useState hook

useState 可以用来管理一个组件比较简单的一两个状态,如果状态多了不适合使用 useState 管理,可以使用 useReducer.

import React, {useState} from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}> Click me </button>
    </div>
  );
}

export default Counter;

在上面的简单例子中,我们直接使用值作为 setState 的参数,但是在部分更新 state 的时候,最好使用一个函数来作为参数:

const [state, setState] = useState({"showText": true, "showShadow": true})

<button onClick={() => setState((s) => {...s, showText: false})}>Hide Text</button>

使用 useEffect hook

useEffect hook 用来实现一些副作用,一般可以用作页面首次加载时读取数据。

import React, {useState, useEffect} from 'react';

function App() {
  const [isOn, setIsOn] = useState(false);

  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(() => console.log('tick'), 1000);
    }
    return () => clearInterval(interval);
  }, [isOn]);
  ...
}

export default App;

在 useEffect 中返回的函数会被用来做垃圾清理。另外需要注意的是,初始化的时候总会触发一次 useEffect.

默认情况下,每次 state 有改变的时候,都会调用 useEffect 函数。如果需要更改触发的时机,那么需要使用 useEffect 的第二个参数来指定监听的事件或者说状态。当第二个参数只使用一个空数组 [] 的时候就只会在组件加载的时候调用。数组中有哪些变量,表示在这些变量变化的时候调用。

一般建议把不依赖 props 和 state 的函数提到你的组件外面,并且把那些仅被 effect 使用的函数放到 effect 里面。

使用 useContext hook

Context 用来向所有包含的元素广播状态,或者说事件,而不需要通过组件树层层传递。

  1. 首先需要通过 React.createContext 定义一个高层次 Context,
  2. 然后在最外层使用 <MyContext.Provider /> 来包裹需要接受这个 context 的所有组件。
  3. 在需要使用状态的元素中调用 const ctx = useContext(MyContext), 然后使用 ctx 访问 Context 中的值

在基于类的 React 组件中,需要使用 <MyContext.Consumer /> 来实现读取值,现在我们都用 useContext 钩子了。

const themes = {
  light: { foreground: "#000000", background: "#eeeeee" },
  dark: { foreground: "#ffffff", background: "#222222" }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div> <ThemedButton /> </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

使用 useReducer hook

useReducer 和 useState 的用途基本是一样的,但是当需要的状态比较复杂的时候,最好使用 useReducer. 有了 useReducer 钩子,基本上可以不使用 redux 了。

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 通常放在管理一组状态的根元素这个层级,比如说一个页面。dispatch 函数触发事件,reducer 函数用来处理事件,更新 state.

需要注意的是,在 useReducer 的 reducer 函数中,和 redux 不同的是,不需要 state=initialState 这个参数。默认参数在调用 useReducer 的时候已经给出了。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useReducer + useContext = (Better) Redux

可以组合使用 useReducer 和 useContext 来实现 Redux 的功能。对于某个组件自身的数据,我们只使用 useReducer 就可以很好地管理了。然而对于多个组件都需要使用的全局数据,需要是用 useContext 来广播给所有需要该数据的组件。

两者的组合使用很简单,使用 Context.Provider 把 state 和 dispatch 这两个变量广播给 root 下的所有元素,这样在需要使用 state 和 dispatch 的地方直接 useContext(Context) 就好了。

useMemo/useCallback 钩子

用来避免重复计算或者重复生成函数。

useRef 钩子

https://stackoverflow.com/questions/56455887/react-usestate-or-useref

自定义钩子

通过灵活组合 useState, 和 useEffect, 我们完全可以创建自己的钩子。

import React from 'react';

function useOffline() {
  const [isOffline, setIsOffline] = React.useState(false);

  function onOffline() {
    setIsOffline(true);
  }

  function onOnline() {
    setIsOffline(false);
  }

  React.useEffect(() => {
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);

    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []);

  return isOffline;
}

function App() {
  const isOffline = useOffline();

  if (isOffline) {
    return <div>Sorry, you are offline ...</div>;
  }

  return <div>You are online!</div>;
}

export default App;

参考

  1. https://www.robinwieruch.de/react-hooks
  2. https://www.robinwieruch.de/react-hooks-fetch-data
  3. https://medium.com/@nazrhan.mohcine/react-hooks-work-with-usestate-and-usereducer-effectively-471646cdf925
  4. https://swizec.com/blog/usereducer-usecontext-for-easy-global-state-without-libraries
  5. https://medium.com/@wisecobbler/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1
  6. https://stackoverflow.com/questions/56615931/react-hook-setstate-arguments
  7. https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

Nextjs 教程

当我们要写一个稍微复杂的 React 应用的时候,就需要路由功能了,比较流行的路由是 react router. 这是一个很好的库,但是当我们已经用到路由的时候,下一步就该考虑如何做服务端渲染了,所以直接上 next.js 吧。

鉴于我已经使用 create-react-app 创建了 react 应用,需要手工安装一下 next.js

yarn add next

把 package.json 中的 scripts 替换掉

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

核心概念

next 的核心概念是页面,没啥可解释的吧。按照约定,放在 /pages 文件夹中的每一个组件都是一个页面,比较恶心的是每个组件需要使用 export default 导出。

当然,pages/index.js 对应的自然是首页了。

function HomePage() {
  return <div>Welcome to Next.js!</div>
}

export default HomePage

然后就可以看到首页啦!啊啊啊

next.js 中,完全按照文件的物理路径来确定路由,比如如果你需要 post/1 这种路径,直接定义 pages/post/[id].js, 也是够直接了。

读取 URL 参数

需要使用 router 来手动读取

import {useRouter} from 'next/router'

function MyPage() {
  const router = useRouter();
  const {keyword} = router.query;
}

获取数据

在 nextjs 中,鼓励的方式是在服务端编译或者渲染的时候获取数据,而不是由客户端渲染数据。这里我们先不看 SSG 了,看现在最需要的 SSR.

在一个页面中,export 一个 async 函数 getServerSideProps 就可以实现获取服务端的数据。

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

context 中比较重要的几个属性:

  • params 路径中的参数,比如 {id: xxx}
  • req/res 请求响应
  • query query_string

在这个函数中,应该直接读取数据库或者外部 API.

除此之外,另一种方式自然是传统的在客户端获取数据了,可以使用 useSWR 库。

样式

next.js 中默认不让导入全局的 CSS, 所以你必须在 pages/_app.js 中导入全局的 css.

import '../styles.css'

// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

对于每一个组件,把它们的样式文件放到 [name].module.css 中就好啦。然后需要导入

import styles from 'Button.module.css';

export default function Button() {
  return <div className={styles.button}>Button</div>
}

另一种方式是使用 styled-jsx, 也就是把 CSS-in-JS 的方式,我个人还是喜欢这种方式一些。但是这种不好在 VSCode 中直接显示调色板。

<style jsx>{`
  h1 {
    color: red;
  }
`}</style>

静态文件

也很简单,直接放到 /public 目录,然后就能在根路径访问了。

路由

使用 [xxx] 放在路径中作为参数就好了。

nextjs 中的链接是这样的:

<Link href="/blog/[slug]" as={`/blog/${post.slug}`}>
    <a>{post.title}</a>
</Link>

外部接口

错误页面

next.js 可以自定义 404 和 500 错误页面

参考

  1. https://haodong.io/render-client-side-only-component-in-next-js
  2. https://github.com/vercel/next.js/blob/canary/examples/progressive-render/pages/index.js

《Prometheus 监控实战》笔记

第一章

与安全性一样,监控也应该是应用程序的核心功能。

如果应用程序在你没有注意到的情况下发生了故障,那么及时进行了监控,你也需要考虑下正在监控的内容是否合
理。

应该监控业务事务的内容或速率,而不是监控它运行的 Web 服务器的运行时间。

静态阈值几乎总是错误的,比如说只监控 CPU 超过 80% 的情况

监控的两种机制:

  • 探针,当我们对要监控的资源没有控制权的时候
  • 内省

显然,应该优先采用内省来监控。但是如果应用程序由第三方提供,并且你没有深入了解其内部操作的时候。从外
部查看应用程序以了解某些网络、安全性或可用性问题通常也很有帮助。

指标是一个组件属性的度量,对这个指标持续跟踪,观察的集合称为时间序列。

单一指标和聚合指标的组合可以提供最佳的健康视图:前者可深入到某个特定问题,而后者可以查看更高阶的状态。

使用平均值描述事件序列是非常危险的。有个笑话是:一位统计学家跳进平均深度只有 25 厘米的湖中,然后差点被
淹死,因为湖中有深达十米的大洞,虽然大部分水面深度只有 10cm

平局值的坏处就在于高峰和低谷可能被平均值所掩盖。百分位数才是识别异常值的理想选择。

监控方法论

Gregg 的 USE 指标:

  • Utilization(使用率). 资源忙于工作的平均时间,通常用随时间变化的百分比表示。
  • Saturatioin(饱和度). 资源排队工作的指标,无法处理额外的工作。通常用队列长度表示。
  • Error(错误). 错误事件的计数

Google 的四个黄金指标

  • 延迟:服务请求所花费的时间,需要区分成功和失败请求。因为失败请求可能延迟非常低,但是结果是错的
  • 流量:QPS 或者 TPS
  • 错误:错误的速率。包括空响应和超时等隐式错误
  • 饱和度:受限的资源,和上面类似

Weaveworks 的 RED 指标

  • Rate(流量)
  • Error(错误率)
  • Duration(延迟)

实际上已经被包含在 Google 的四个指标中了

警报和通知

  • 哪些问题需要通知
  • 谁需要被告知
  • 如何告知他们
  • 多久告知他们一次
  • 何时停止通知或升级到其他人

通知的信息应该包含以下几方面:

  • 清晰准确,可操作。应该让人能够看懂并知道如何操作
  • 为通知添加上下文,应该包含其他组件的相关信息
  • 只发送有意义的通知,不要因为有了通知系统就一定要使用

可视化

可视化系统最终要的在于:突出重点而不仅是提升视觉效果。

第二章

大多数监控查询和警报都是从最近(通常是一天内)的数据产生的。

Prometheus 的高可用架构建议:

  • 使用两个或多个相同配置的 Prometheus 服务器收集指标
  • 所有生成的警报发送到一个 Alertmanger 的集群,由 altermanager 进行消重

第三章

强烈建议不要单独配置每个服务的指标抓取间隔,这样能够确保你的所有时间序列具有相同的粒度。

即使向量 (Instant Vector): 一组包含每个时间序列的单个样本的时间序列集合

第四章

cAdvisor 作为容器运行,可以用来监控 Docker

如何设定标签体系?

  1. 使用拓扑标签,比如说 datacenter, job, instance 等
  2. 使用模式标签,比如 url_path, error_code

在这里书中有一处错误,书中说可以使用 user 作为标签,实际上绝对不要用 user 作为标签,这会让时间序列的 rank 直接爆炸

TODO avg(rate()) 是啥意思

predict_linear 这个函数非常有用,可以用来回答:”考虑到现在磁盘使用情况,以及他的增长速率,我们会在什么时候耗尽磁盘空间?

对于向量匹配运算,在多数情况下,一对一匹配就足够了。

第六章

最常见的错误是发送过多的警报。

应该针对症状而不是原因发出警报,又人类来判断造成问题的具体原因。

Alertmanger 的 route 块配置警报如何处理。receivers 配置警报的目的地。在规则中 expr 配置触发警报的规
则,而 for 指定在触发警报之前,测试表达式必须为 true 的时长,annotations 用于指定展示更多信息的标签。

警报一共有三个状态:

  • Inactive
  • Pending, 已经为 true, 但是 for 的时间还没满足
  • Firing, 处于触发状态

如果不指定 for 子句,那么警报会直接由 Inactive 转为 Firing.

在 annotation 中还可以使用模板,其中有一些变量,和 humanize 等等函数。

routes 是树形的,在 Yaml 配置中直接嵌套。

routes:
- match:
    severity: critical
  receiver: pager
  routes:
    - match:
        servrity: application
      receiver: support_team

Alert 的 silence 也很重要,可以通过

  • web 控制台
  • amtool
  • unsee 等第三方控制台

第七章

Prometheus 认为实现集群所投入的成本要高于数据本身的价值,所以 Prometheus 不用集群,直接两个配置走起。

第八章

为应用程序添加监控,从以下入口和出口做起:

  1. 测量请求和响应的数量和时间
  2. 测量对外部服务和 API 的调用次数和时间,比如对于数据库等的调用
  3. 测量作业调度,执行和其他周期性事件
  4. 测量重要业务和功能性事件的数量和时间

应用程序的指标又分为两大类

  1. 技术性指标,用于衡量应用程序的性能和状态,比如吞吐量、功能和状态等等
  2. 业务性指标,用于衡量应用程序的价值,比如电商网站的销售量

业务指标通常是应用程序指标的更进一步,他们通常与技术指标同义。一个技术性指标可能是交易服务的延迟,
而业务性指标可能是每个交易的价值

另外,对于长期业务指标来说,不要用监控系统了,一般来说还是用基于事件的统计系统。

第九章

mtail 用于从日志中抽取并发送时间序列

第十章

探针监控也称为黑盒监控

第十一章

当目标端点没有可以抓取的端点,比如批处理作业,可以使用 push gateway

Push Gateway 开箱即用,没有什么配置。

Web Cron 市场调研

因为实现了 sche 的语法,感觉完爆 cron,所以调研了一下 web cron,不过似乎市场不大。这个算是 unboundlling AWS 的一种,但是功能太简单了,直接用 AWS 也不复杂。而且竞争对手太多了。。

Heroku 真的是个很有意思的服务,解决了很多痛点,但是项目还是挺大的。不过值得思考的是,为什么 Heroku 做起来了,而 Google App Engine 凉了?是因为 GAE 超前时代太多了吗?

Sche.io 还没有被注册,可惜 99 刀太贵了,不然先注册一把。

  1. Indie Hacker 上有人有类似的想法。https://www.indiehackers.com/forum/cloud-based-cron-job-should-i-built-it-c46f58e66f
  2. Hacker News 上的评论大多是正面的,但是也不知道会不会付费。https://news.ycombinator.com/item?id=17346616
  3. 直接用 aws lambda 就行了。。https://gist.github.com/milesw/83332215df29fa25239712cd1ba273d9
  4. 这个 Cronhub 号称挣钱了。但是没看出哪里有意思或者可复制来。https://www.indiehackers.com/@tigran/cronhub-2nd-month-report-9e474add23

提到的一些痛点:

  1. 如何做好授权?
  2. 不一定所有服务都暴露了外部 http 接口
  3. 对于长时间执行的任务怎么办
  4. 能否直接 ssh 上去执行任务呢?
  5. 能否提供 API 让用户通过代码上传任务

如果真要做的话,应该把监控、日志统计等都做了。做到比 AWS、Azure、Heroku 的调度功能更加方便。

比较有意思的思考

网站的监控是一个挺大的领域

  1. 包括网站的功能监控,比如 Pingdom,HyperPing,PagerDuty,DataDog 等等
  2. 网站的用户监控,Google Analytics 和一系列的工具

关于 Heroku vs App Engine

这篇文章说的不错。Google 的第一个问题在于没有把用户放在心里,而是在 Demo 甚至 Show off 自己的 Infra。比如说:

  • 在 GAE 上用户需要大幅度修改自己的代码才能运行
  • 没有 SQL,只能用 Google 自己的 BigTable
  • 不支持最最最流行的 PHP

等等。。

而在 Heroku 上,用户可以随意使用已经很熟悉的 Postgres/Mongo/Redis/MySQL 等等数据库,也就是说迁移到 Heroku 几乎不费任何代价,而且还不用关心负载平衡、监控等运维细节。

另一方面,Heroku 最开始支持的是 Ruby on Rails,而这个社区在当时是非常活跃,而且乐于传教的。

最后一点,Google App Engine 甚至比 Heroku 贵不少。。说好的大厂不差钱呢?

sche – 一种人类能够看懂的 cron 语法

在 Linux 系统上,我们一般使用 cron 来设置定时任务,然而 cron 的语法还是有些佶屈聱牙的,几乎每次要修改的时候都需要查一下文档才知道什么意思,以至于有 crontab.guru 这种网站专门来解释 cron 的语法。

想象一下,能不能有一种让人一眼就能看懂的语法来表达周期性的调度操作呢?比如说这样:

every 10 minutes         , curl apple.com
every hour               , echo 'time to take some coffee'
every day at 10:30       , eat
every 5 to 10 minutes    , firefox http://news.ycombinator.com
every monday             , say 'Good week'
every wednesday at 13:15 , rm -rf /
every minute at :17      , ping apple.com
every 90 minutes         , echo 'time to stand up'

这样的配置文件是不是很容易懂呢?如果要写成 crontab 的格式大概是这样的:

*/10 * * * *    curl apple.com
0 * * * *       echo 'time to take some coffee'
30 10 * * *     eat
*/7 * * * *     firefox http://news.ycombinator.com  # 实际上是不对的,因为 cron 没法随机
0 0 * * MON     say 'Good week'
15 13 * * WED   rm -rf /
# every minute at :17  无法实现,因为 cron 中没有秒
0 0-21/3 * * *  echo 'time to stand up'  # 需要两条命令来完成每隔 90 分钟的操作
30 1-22/3 * * * echo 'time to stand up'

可以很明显看出,cron 的语法可读性还是差一些的,关键是维护起来更是像读天书一样。幸运的是,我在周末刚刚做了一个小工具,虽然还比较粗糙,但是也已经可以解析上面这种可读性比较好的语法。下面简单介绍一下如何使用:

介绍 sche

sche 是一个 Python 程序,所以可以使用 pip 直接安装:

pip install sche

安装之后,就会得到一个 sche 命令,帮助文件如下:

-> % sche -h
usage: sche [-h] [-f FILE] [-t]

A simple command like `cron`, but more human friendly.

The default configuration file is /etc/schetab, syntax goes like:

    # (optional) set time zone first
    timezone = +0800

    # line starts with # is a comment
    every 60 minutes, echo "wubba lubba dub dub"

    # backup database every day at midnight
    every day at 00:00, mysqldump -u backup

    # redirect logs so you can see them
    every minute, do_some_magic >> /some/output/file 2>&1

optional arguments:
  -h, --help            show this help message and exit
  -f FILE, --file FILE  configuration file to use
  -t, --test            test configuration and exit

我们只需要把需要执行的命令放到 /etc/schetab 文件下就好了,这里显然是在致敬 /etc/crontab。比如说:

-> % cat /etc/schetab
timzone = +0800
every 5 seconds, echo "wubba lubba dub dub"
every 10 seconds, date

-> % sche
wubba lubba dub dub
Tue Sep  1 22:15:01 CST 2020
wubba lubba dub dub
wubba lubba dub dub
Tue Sep  1 22:15:11 CST 2020
wubba lubba dub dub
wubba lubba dub dub
Tue Sep  1 22:15:21 CST 2020
wubba lubba dub dub

如何让 sche 像 cron 一样作为一个守护进程呢?秉承 Unix 一个命令只做一件事的哲学,sche 本身显然是不提供这个功能的,可以使用 systemd 实现,几行配置写个 unit 文件就搞定了。

sche 的来源

sche 是 schedule — 另一个 Python 库的一个 fork, schedule 支持这样的 Python 语句:

schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)
schedule.every().minute.at(":17").do(job)

然而我的需求是把时间配置独立出来,能够像 cron 一样存到一个文本文件里,而不是写成 Python 代码,于是提了一个 PR,增加了 when 这个方法来解析表达式。同时我还强烈需求时区支持,然而原版的 schedule 也不支持。所以就创建了一个 fork.

sche.when("every wednesday at 13:15").do(job)
sche.timezone("+0800").every().day.at("00:00").do(job)

最后,原生的 cron 命令实际上(至少我)已经极少用了,然而 crontab 的语法流传还是非常广的,在所有需要定时任务的地方,几乎都能看到 cron 的身影,比如说 Kubernetes job 等等,如果能够使用一种让正常人能随时看懂的语法,感觉还是很有意义的。

参考

  1. https://schedule.readthedocs.io/en/stable/
  2. https://crontab.guru/
  3. https://stackoverflow.com/q/247626/1061155