在main之前,IAR都做了什么

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在main之前,IAR都做了什么相关的知识,希望对你有一定的参考价值。

参考技术A 首先系统复位时,Cortex-M3从代码区偏移0x0000'0000处获取栈顶地址,用来初始化MSP寄存器的值。

接下来从代码区偏移0x0000'0004获取第一个指令的跳转地址。这些地址,是CM3要求放置中断向量表的地方。

这里是一个程序的启动区的反汇编:

__vector_table:
08004000 2600

08004002 2000

08004004 7E1D

08004006 0800

这个程序是由IAP程序来启动的,IAP程序获取0x0800'4000处的MSP值(0x20002600),并设置为MSP的值,即主堆栈最大
范围是0x2000'0000~0x2000'25FF。接下来IAP程序获取0x0800'4004处的Reset_Handler的地址
(0x0800'7E1D),并跳转到Reset_Handler()执行。

IAP在这里完全是模仿了Cortex-M3的复位序列,也就是说,在没有IAP的系统上,CM3只能从0x0800'0000获取MSP,从
0x0800'0004获取第一条指令所处地址。而IAP就存在在0x0800'0000这个地址上,IAP的启动,已经消耗掉了这个复位序列,所以
IAP要启动UserApp程序的时候,也是完全模仿Cortex-M3的复位序列的。

接下来我们看看复位后第一句指令——Reset_Handler()函数里有什么。

若我们使用的是ST公司标准外设库,那么已经有了现成的Reset_Handler,不过他是弱定义——PUBWEAK,可以被我们重写的同名函数覆盖。一般来说,我们使用的都是ST提供的Reset_Handler,在V3.4版本的库中,可以在startup_stm32f10x_xx.s中找到这个函数:

PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER(2)
Reset_Handler
LDR R0, =SystemInit
BLX R0
LDR R0, =__iar_program_start
BX R0

看来ST没有做太多的事,他只调用了自家库提供的SystemInit函数进行系统时钟、Flash读取的初始化,并把大权交给了
__iar_program_start这个IAR提供的“内部函数”了,我们就跟紧这个__iar_program_start跳转,看看IAR做了什
么,上面一段代码的反汇编如下:

Reset_Handler:
__iar_section$$root:
08007E1C 4801 LDR R0, [PC, #0x4]; LDR R0, =SystemInit
08007E1E 4780 BLX R0;BLX R0
08007E20 4801 LDR R0, [PC, #0x4];LDR R0, =__iar_program_start
08007E22 4700 BX R0;BX R0
08007E24 6C69

08007E26 0800

08007E28 7D8D

08007E2A 0800

细心的观众会发现地址是0x0800'7E1C,比我们查到的0x0800'7E1D差了1,这是ARM家族的遗留问题,因为ARM处理器的指令至
少是半字对齐的(16位THUMB指令集 or
32位ARM指令集),所以PC指针的LSB是常为0的,为了充分利用寄存器,ARM公司给PC的LSB了一个重要的使命,那就是在执行分支跳转时,PC
的LSB=1,表示使用THUMB模式,LSB=0,表示使用ARM模式,但在最新的Cortex-M3内核上,只使用了THUMB-2指令集挑大梁,所
以这一位要常保持1,所以我们查到的地址是0x0800'7E1D(C=1100,D=1101),放心,我们的CM3内核会忽略掉LSB(除非为0,那
么会引起一个fault),从而正确跳转到0x0800'7E1C。

从0x0800'7E20处的加载指令,我们可以算出__iar_program_start所处的位置,就是当前PC指针
(0x0800'7E24),再加上4,即0x0800'7E28处的所指向的地址——0x0800'7D8D(0x0800'7D8C),我们跟紧着跳
转,__iar_program_start果然在这里:

__iar_program_start:
08007D8C F000F88C BL __low_level_init
08007D90 2800 CMP R0, #0x0
08007D92 D001 BEQ __iar_init$$done
08007D94 F7FFFFDE BL __iar_data_init2

08007D98 2000 MOVS R0, #0x0
08007D9A F7FDFC49 BL main

我们看到IAR提供了__low_level_init这个函数进行了“底层”的初始化,进一步跟踪,我们可以查到__low_level_init这个函数做了些什么,不是不是我们想象中的不可告人。

__low_level_init:
08007EA8 2001 MOVS R0, #0x1
08007EAA 4770 BX LR

__low_level_init出乎想象的简单,只是往R0寄存器写入了1,就立即执行"BX
LR"回到调用处了,接下来,__iar_program_start检查了R0是否为0,为0,则执行__iar_init$$done,若不是0,就
执行__iar_data_init2。__iar_init$$done这个函数很简单,只有2句话,第一句是把R0清零,第二句就直接"BL
main",跳转到main()函数了。不过既然__low_level_init已经往R0写入了1,那么我们还是得走下远路——看看
__iar_data_init2做了些什么,虽然距离main只有一步之遥,不过这中间隐藏了编译器的思想,我们得耐心看下去。

__iar_data_init2:
08007D54 B510 PUSH R4,LR
08007D56 4804 LDR R0, [PC, #0x10]
08007D58 4C04 LDR R4, [PC, #0x10]
08007D5A E002 B 0x8007D62
08007D5C F8501B04 LDR R1, [R0], #0x4
08007D60 4788 BLX R1
08007D62 42A0 CMP R0, R4
08007D64 D1FA BNE 0x8007D5C
08007D66 BD10 POP R4,PC
08007D68 7C78

08007D6A 0800

08007D6C 7C9C

08007D6E 0800

看来IAR迟迟不执行main()函数,就是为了执行__iar_data_init2,我们来分析分析IAR都干了些什么坏事~

首先压R4,LR入栈,然后加载0x0800'7C78至R0,0x0800'7C9C至
R4,马上跳转到0x0800'7D62执行R0,R4的比较,结果若是相等,则弹出R4,PC,然后立即进入main()。不过IAR请君入瓮是自不会
那么快放我们出来的——结果不相等,跳转到0x0800'7D5C执行,在这里,把R0指向的地址——0x0800'7C78中的值——
0x0800'7D71加载到R1,并且R0中的值自加4,更新为0x0800'7C7C,并跳转到R1指向的地址处执行,这里是另一个IAR函
数:__iar_zero_init2:

__iar_zero_init2:
08007D70 2300 MOVS R3, #0x0
08007D72 E005 B 0x8007D80
08007D74 F8501B04 LDR R1, [R0], #0x4
08007D78 F8413B04 STR R3, [R1], #0x4
08007D7C 1F12 SUBS R2, R2, #0x4
08007D7E D1FB BNE 0x8007D78
08007D80 F8502B04 LDR R2, [R0], #0x4
08007D84 2A00 CMP R2, #0x0
08007D86 D1F5 BNE 0x8007D74
08007D88 4770 BX LR
08007D8A 0000 MOVS R0, R0

__iar_data_init2还没执行完毕,就跳转到了这个__iar_zero_inti2,且看我们慢慢分析这个帮凶——__iar_zero_inti2做了什么。

__iar_zero_inti2将R3寄存器清零,立即跳转到0x0800'7D80执行'LDR R2, [R0],
#0x4',这句指令与刚才在__iar_data_init2见到的'LDR R1, [R0],
#0x4'很类似,都为“后索引”。这回,将R0指向的地址——0x0800'7C7C中的值——0x0000'02F4加载到R2寄存器,然后R0中的
值自加4,更新为0x0800'7C80。接下来的指令检查了R2是否为0,显然这个函数没那么简单想放我我们,R2的值为2F4,我们又被带到了
0x0800'7D74处,随后4条指令做了如下的事情:

1、将R0指向的地址——0x0800'7C80中的值——0x2000'27D4加载到R1寄存器,然后R0中的值自加4,更新为0x0800'7C84。

2、将R1指向的地址——0x2000'27D4中的值——改写为R3寄存器的值——0,然后R1中的值自加4,更新为0x2000'27D8。

3、R2自减4

4、检查R2是否为0,不为0,跳转到第二条执行。不为,则执行下一条。

这简直就是一个循环!——C语言的循环for(r2=0x2F4;r2-=4;r!=0)...,我们看看循环中做了什么。

第一条指令把一个地址加载到了R1——0x2000'27D4

是一个RAM地址,以这个为起点,在循环中,对长度为2F4的RAM空间进行了清零的操作。那为什么IAR要做这个事情呢?消除什么记录么?用Jlink
查看这片内存区域,可以发现这片区域是我们定义的全局变量的所在地。也就是说,IAR在每次系统复位后,都会自动将我们定义的全局变量清零0。

清零完毕后,接下来的指令"LDR R2, [R0],
#0x4"将R0指向的地址——0x0800'7C84中的值——0加载到R2寄存器,然后R0中的值自加4,更新为0x0800'7C88。随后检查
R2是否为0,这里R2为0,执行'BX
LR'返回到__iar_data_init2函数,若是不为0,我们可以发现又会跳转至“4指令”处进行一个循环清零的操作。

开发和运维对K8S中的应用都做了什么?

微信公众号:运维开发故事,作者:乔克

在应用的整个生命周期里,开发和运维都和它密不可分。一个塑造它,一个保养它。

如果应用需要部署到K8S中,开发和运维在其中都做了什么呢?

开发侧

从开发侧来说,我们的应用应该具备以下能力:

  • 具有健康检测接口
  • 具有优雅退出能力
  • 具有metrics接口
  • 能够接入链路追踪系统
  • 日志输出标准统一

定义健康检测接口

健康检测接口用于检测应用的健康状态,在K8S中,使用Readiness和Liveness分别来探测应用是否就绪和是否存活,如果未就绪或者未存活,K8S会采取相应的措施来确保应用可用。

如果我们应用未定义好相应的健康检测接口,K8S就无法判断应用是否正常可用,整个应用对我们来说就是黑匣子,也就谈不上应用稳定性了。

定义一个简单的健康检测接口如下:

package router

import (
"github.com/gin-gonic/gin"
v1 "go-hello-world/app/http/controllers/v1"
)

func SetupRouter(router *gin.Engine)
ruc := new(v1.RootController)
router.GET("/", ruc.Root)

huc := new(v1.HealthController)
router.GET("/health", huc.HealthCheck)
package v1

import (
"github.com/gin-gonic/gin"
"go-hello-world/app/http/controllers"
"go-hello-world/pkg/response"
"net/http"
)

type HealthController struct
controllers.BaseController


func (h *HealthController) HealthCheck(c *gin.Context)
response.WriteResponse(c, http.StatusOK, nil, gin.H
"result": "健康检测页面",
"status": "OK",
)

如上我们定义了​​health​​接口,当应用启动后,只需要探测这个接口,如果返回OK,表示应用是正常的。

当然,上面的接口是非常简单的,在实际情况下,应用本身也许还依赖起来应用,比如redis,mysql,mq等,如果它们异常,应用是不是异常的呢?那我们的应用健康检测需不需要检测其他应用的健康状态呢?

既然我们定义好了健康检测接口,那我们的YAML模板就可以增加健康检测功能,如下:

readinessProbe:
httpGet:
path: /health
port: http
timeoutSeconds: 3
initialDelaySeconds: 20
livenessProbe:
httpGet:
path: /health
port: http
timeoutSeconds: 3
initialDelaySeconds: 30

定义优雅下线功能

应用发版是常规不能再常规的操作,通常情况下都是滚动更新的方式上线,也就是先起一个新应用,再删一个老应用。

如果这时候老应用有部分的流量,突然把老应用的进程杀了,这部分流量就无法得到正确的处理,部分用户也会因此受到影响。

怎么才会不受影响呢?

假如我们在停止应用之前先告诉网关或者注册中心,等对方把我们应用摘除后再下线,这样就不会有任何流量受到影响了。

在K8S中,当我们要删除Pod的时候,Pod会变成Terminating状态,kubelet看到Pod的状态如果为Terminating,就会开始执行关闭Pod的流程,给Pod发SIGTERM信号,如果达到宽限期Pod还未结束就给Pod发SIGKILL信号,从Endpoints中摘除Pod等。

从上面可知,Pod在停止之前会收到SIG信号,如果应用本身没有处理这些信号的能力,那应用如果知道什么时候该结束呢?

下面简单定义一个处理SIG信号的功能。

package shutdown

import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
)

// 优雅退出

type Shutdown struct
ch chan os.Signal
timeout time.Duration


func New(t time.Duration) *Shutdown
return &Shutdown
ch: make(chan os.Signal),
timeout: t,



func (s *Shutdown) Add(signals ...os.Signal)
signal.Notify(s.ch, signals...)


func (s *Shutdown) Start(server *http.Server)
<-s.ch
fmt.Println("start exist......")

ctx, cannel := context.WithTimeout(context.Background(), s.timeout*time.Second)
defer cannel()
if err := server.Shutdown(ctx); err != nil
fmt.Println("Graceful exit failed. err: ", err)

fmt.Println("Graceful exit success.")
package main

import (
"github.com/gin-gonic/gin"
"go-hello-world/pkg/shutdown"
"go-hello-world/router"
"log"
"net/http"
"syscall"
"time"
)


func main()
r := gin.New()

// 注册路由
router.SetupRouter(r)

server := &http.Server
Addr: ":8080",
Handler: r,


// 运行服务
go func()
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed
log.Fatalf("server.ListenAndServe err: %v", err)

()

// 优雅退出
quit := shutdown.New(10)
quit.Add(syscall.SIGINT, syscall.SIGTERM)
quit.Start(server)

当接收到SIG信号的时候,就会调用​​Shutdown​​方法做应用退出处理。

除此,还要结合K8S的​​PreStop Hook​​来定义结束前的钩子,如下:

lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 30

如果使用注册中心,比如nacos,我们可以在​​PreStop Hook​​中先告诉nacos要下线,如下:

lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "curl -X DELETE your_nacos_ip:8848/nacos/v1/ns/instance?serviceName=nacos.test.1&ip=$POD_IP&port=8880&clusterName=DEFAULT" && sleep 30

定义Metrics接口

Metrics主要用来暴露应用指标,可以根据实际情况自定义指标,以便于监控工具Prometheus进行数据收集展示。

有些语言有现成的exporter,比如java的jmx_exporter,没有的就需要自己在应用中集成。

比如:

package main

import (
"github.com/SkyAPM/go2sky"
v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
"github.com/SkyAPM/go2sky/reporter"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go-hello-world/pkg/shutdown"
"go-hello-world/router"
"log"
"net/http"
"syscall"
"time"
)

var SKYWALKING_ENABLED = false

func main()
r := gin.New()

// 注册路由
router.SetupRouter(r)

server := &http.Server
Addr: ":8080",
Handler: r,


// 启动metrics服务
go func()
http.Handle("/metrics", promhttp.Handler())
if err := http.ListenAndServe(":9527", nil); err != nil
log.Printf("metrics port listen failed. err: %s", err)

()

// 运行服务
go func()
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed
log.Fatalf("server.ListenAndServe err: %v", err)

()

// 优雅退出
quit := shutdown.New(10)
quit.Add(syscall.SIGINT, syscall.SIGTERM)
quit.Start(server)

这种会暴露默认的Http指标,可以通过​​curl 127.0.0.1:9527/metrics​​获取指标。

......
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_totalcode="200" 0
promhttp_metric_handler_requests_totalcode="500" 0
promhttp_metric_handler_requests_totalcode="503" 0

如果需要自定义指标的话,只需按规则定义即可,如下:

package metrics

import (
"github.com/prometheus/client_golang/prometheus"
"net/http"
"time"
)

var (
// HttpserverRequestTotal 表示接收http请求总数
HttpserverRequestTotal = prometheus.NewCounterVec(prometheus.CounterOpts
Name: "httpserver_request_total",
Help: "The Total number of httpserver requests",
,
// 设置标签:请求方法和路径
[]string"method", "endpoint")

HttpserverRequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts
Name: "httpserver_request_duration_seconds",
Help: "httpserver request duration distribution",
Buckets: []float640.1, 0.3, 0.5, 0.7, 0.9, 1,
,
[]string"method", "endpoint")
)

// 注册监控指标
func init()
prometheus.MustRegister(HttpserverRequestTotal)
prometheus.MustRegister(HttpserverRequestDuration)


func NewMetrics(router http.HandlerFunc) http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request)
start := time.Now()
router(w, r)
duration := time.Since(start)
// httpserverRequestTotal 记录
HttpserverRequestTotal.With(prometheus.Labels"method": r.Method, "endpoint": r.URL.Path).Inc()
// httpserverRequestDuration 记录
HttpserverRequestDuration.With(prometheus.Labels"method": r.Method, "endpoint": r.URL.Path).Observe(duration.Seconds())

这样就定义了​​httpserver_request_total​​​和​​httpserver_request_duration_seconds​​​指标,引用过后就能在​​/metrics​​中看到对应的数据。

定义好了指标,下面就是收集了。既可以通过自定义收集规则收集,也可以通过自动发现的方式收集,为了方便,主要采用自动发现的方式。

我们只需要在deployment的templates中定义好annotation,prometheeus就会自动添加采集目标,如下:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: httpserver
name: httpserver
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: httpserver
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "metrics"
labels:
app: httpserver
spec:
containers:
image: baidjay/httpserver:ubuntu-v3-metrics
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 15
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: http
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3
name: httpserver
ports:
- containerPort: 8080
name: http
protocol: TCP
- name: metrics
protocol: TCP
containerPort: 9527
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: http
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3

定义Trace功能

Trace用于跟踪,每个请求都会生成一个​​TraceID​​,这个ID会伴随请求的整个生命周期,我们也可以根据这个ID查询请求的整个链路情况。

链路追踪,目前市面上有很多开源系统,比如Skywalking,Jeager,Zipkin等,它们各有各的特点,如下。


Pinpoint

Zipkin

Jaeger

Skywalking

OpenTracing兼容

客户端支持语言

java\\php

java\\go\\php等

java\\go\\php等

java\\nodejs\\php等

存储

hbase

es\\mysql\\内存等

es\\kafka\\内存等

es\\mysql\\h2等

传输协议支持

thrift

http\\mq

udp\\http

grpc

UI丰富程度

实现方式

字节码注入

拦截请求

拦截请求

字节码注入

扩展性

Trace查询

不支持

支持

支持

支持

告警支持

支持

不支持

不支持

支持

JVM监控

支持

不支持

不支持

支持

性能损失

我比较推荐使用Jaeger,它是CNCF的毕业项目,成长空间和云原生的系统架构兼容性比较好。

不过,我这里采用的Skywalking。

Skywalking有许多现成的客户端,比如Java、Python等,可以直接使用,它们都会自动埋点,但是对于Go来说就只有自己手动埋点了,需要我们自己去写代码。

比如:

package main

import (
"github.com/SkyAPM/go2sky"
v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
"github.com/SkyAPM/go2sky/reporter"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go-hello-world/pkg/shutdown"
"go-hello-world/router"
"log"
"net/http"
"syscall"
"time"
)

var SKYWALKING_ENABLED = false

func main()
r := gin.New()

// 配置skywalking
if SKYWALKING_ENABLED
rp, err := reporter.NewGRPCReporter("skywalking-oap:11800", reporter.WithCheckInterval(time.Second))
if err != nil
log.Printf("create gosky reporter failed. err: %s", err)

defer rp.Close()
tracer, _ := go2sky.NewTracer("go-hello-world", go2sky.WithReporter(rp))
r.Use(v3.Middleware(r, tracer))


// 注册路由
router.SetupRouter(r)

server := &http.Server
Addr: ":8080",
Handler: r,


// 启动metrics服务
go func()
http.Handle("/metrics", promhttp.Handler())
if err := http.ListenAndServe(":9527", nil); err != nil
log.Printf("metrics port listen failed. err: %s", err)

()

// 运行服务
go func()
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed
log.Fatalf("server.ListenAndServe err: %v", err)

()

// 优雅退出
quit := shutdown.New(10)
quit.Add(syscall.SIGINT, syscall.SIGTERM)
quit.Start(server)

定义reporter用于上报数据给Skywalking,这就是一个简单的集成Trace的例子。

定义标准的日志

应用的可观测性主要来源日志、监控、链路追踪,标准的日志有利于日志收集以及排查问题。

原则上,不论是什么类型的日志输出,什么格式的日志内容,都能收集。但是为了方便友好,建议把日志输出到标准输出,这样收集更方便。

我个人理解,在K8s中,完全没必要把日志输出到文件,浪费不说,没多大意义,因为所有的日志我们都会收集到日志系统,而输出到文件的日志也会随着应用发版而丢失,所以输出到文件的意义是什么呢?

运维侧

开发把系统开发完,就会交付给运维部署。为了保障应用的稳定性,运维在部署应用的时候应该考虑以下几点。

  • 应用尽可能保持无状态
  • 应用尽可能保持高可用
  • 应该具备优雅上线能力
  • 应该具备异常自愈能力
  • 可以使用HTTPS访问

应用尽可能保持无状态

K8S中可以部署有状态应用,也可以部署无状态应用。对于有状态应用,我其实很少部署到K8S中,大部分还是部署的无状态应用,至于为什么,用多了就晓得了。

对于业务应用,强烈建议使其保持无状态,就算有需要持久化的东西,要么保存到数据库,要么保存到对象存储或者其他单独的文件系统中,不要挂载到应用Pod上。

这样的好处是,应用和数据是分开的,应用可以随意启停、扩展、迁移等。

应用尽可能的保持高可用

保持高可用应该是每个运维人员的使命。

在K8S中,我们应该怎么配置呢?(1)应用Pod应该是多副本

(2)应用Pod之间做反亲和性,避免同一应用调度到同一台主机,如下。

......
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: [ "httpserver" ]
topologyKey: kubernetes.io/hostname
......

(3) 为了避免应用因为节点维护等原因驱逐Pod,导致全部Pod被驱逐,特别配置了PodDisruptionBudget,保障应用至少有一个可用,如下。

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: httpserver
spec:
minAvailable: 1
selector:
matchLables:
app: httpserver

(4)如果某个节点因为一些原因需要驱逐一些Pod,为了避免重要应用被驱逐,应该给应用配置较高的QoS,如下:

resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: "1"
memory: 2Gi

应用具备优雅上线能力

所谓优雅上线能力,就是要确保应用能够提供服务了,再接入外界流量,不能在还没完全启动的情况下就提供服务。

在K8S中,应用在启动后会加入endpoints中,然后通过service接入流量,那在什么情况下才算启动成功呢?主要是通过K8S的​​ReadinessProbe​​来进行检测。这时候开发的健康检测接口就派上用场了,如下:

...
readinessProbe:
failureThreshold: 3
httpGet:
path: /health
port: http
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3
...

所以我们K8S的YAML文件应该加上如上的配置。

应该具备异常自愈能力

所谓异常自愈,就是应用本身在出现Crash,或者应用Pod所在节点出现异常的情况,应用能够自动重启或者迁移。这时候就需要通过K8S的​​LivenessProbe​​来进行检测了,如下。

......
livenessProbe:
failureThreshold: 3
httpGet:
path: /health
port: http
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3
......

当K8S的YAML清单加上如上配置过后,就会定时去探测应用是否正常,如果异常,就会触发重启的动作。如果是节点异常,K8S会对Pod进行重新调度。

可以使用HTTPS进行访问

应用通过HTTPS访问是比较常见的,企业级应用建议自己购买相应的SSL证书,然后进行配置即可。

比如。

# 创建证书secret
kubectl create secret tls httpserver-tls-secret --cert=path/to/tls.cert --key=path/to/tls.key
# 在ingress中引用
......
spec:
tls:
hosts:
- httpserver.coolops.cn
secretName: httpserver-tls-secret
rules:
- host: httpserver.coolops.cn
......

总结

上面介绍了开发和运维对于应用上线应该做的工作,​​不全但够用​​。

在不同的企业都有不同的尿性,但是作为运维,我们都要牢牢记住​​稳定​​永远是第一尿性。通过上面的梳理,我们的应用模板就整理如下:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: httpserver
name: httpserver
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
app: httpserver
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "metrics"
labels:
app: httpserver
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: [ "httpserver" ]
topologyKey: kubernetes.io/hostname
containers:
- env:
- name: TZ
value: Asia/Shanghai
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
image: baidjay/httpserver:ubuntu-v3-metrics
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 15
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: http
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3
name: httpserver
ports:
- containerPort: 8080
name: http
protocol: TCP
- name: metrics
protocol: TCP
containerPort: 9527
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: http
scheme: HTTP
initialDelaySeconds: 20
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: "1"
memory: 2Gi
securityContext:
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
---
apiVersion: v1
kind: Service
metadata:
name: httpserver
spec:
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
- name: metrics
port: 9527
protocol: TCP
targetPort: metrics
selector:
app: httpserver
sessionAffinity: None
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 100m
nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/service-weight: ""
nginx.org/client-max-body-size: 100m
name: httpserver-tls
spec:
tls:
- hosts:
- httpserver.coolops.cn
secretName: httpserver-tls-secret
rules:
- host: httpserver.coolops.cn
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: httpserver
port:
number: 8080
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: httpserver
spec:
minAvailable: 1
selector:
matchLabels:
app: httpserver



我是 乔克,《运维开发故事》公众号团队中的一员,一线运维农民工,云原生实践者,这里不仅有硬核的技术干货,还有我们对技术的思考和感悟,欢迎关注我们的公众号,期待和你一起成长!

以上是关于在main之前,IAR都做了什么的主要内容,如果未能解决你的问题,请参考以下文章

[转发]在main()之前,IAR都做了啥?

不是问题的问题为什么复位中断服务程序里面直接调用的main函数,难道所有程序都在复位中断里面执行的?

[转]nginx启动期都做了哪些事

深入分析Java的编译期与运行期

NSThread的main方法内部做了什么?

Spring Cloud都做了些什么