레이블이 golang인 게시물을 표시합니다. 모든 게시물 표시
레이블이 golang인 게시물을 표시합니다. 모든 게시물 표시

golang gomaxprocs in k8s

golang garbage collector(GC) 는 데이터 무결성을 위해 stop-the-world(일시정지)가 필요하다.

linux 스케줄러 Completely Fair Scheduler(CFS) 에서 프로세스를 cpu(core) 시간에 할당한다.
golang 은 container(linux cfs 기반)의 cpu (시간)제한을 인지하지 못해 일시정지가 발생할 수 있다.

GOMAXPROCS 를 k8s cpu limit 와 일치 시키면 gc 로 인한 일시정지를 줄 일 수 있다.
GOMAXPROCS 를 k8s cpu limit 와 일치시키기
ubuer automaxprocs 를 golang main 에 import 하면 되지만 GOMEMLIMIT 는 지원하지 않는다.

대신 환경변수로 GOMAXPROCS, GOMEMLIMIT 로 pod > resource > limits 를 설정하면 된다.

golang main() 에 다음을 추가해 프로그램 시작시 GOMAXPROCS, GOMEMLIMIT 값을 찍어보자.
기본적으로 최대 core 개수가 찍힌다.
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Printf("GOMEMLIMIT: %d\n", debug.SetMemoryLimit(-1))

이제 deployment 에 다음과 같이 GOMAXPROCS, GOMEMLIMIT 환경변수를 적용하면
pod(container)가 새로 시작되고 golang 프로그램의 GOMAXPROCS, GOMEMLIMIT 에 반영된다.
spec:
  template:
    spec:
      containers:
      - name: ysoftman-app
        resources:
          requests:
            cpu: "2000m"
            memory: "2048Mi"
          limits:
            cpu: "2000m"
            memory: "2048Mi"
        env:
        - name: GOMEMLIMIT
          valueFrom:
            resourceFieldRef:
              resource: limits.memory
        - name: GOMAXPROCS
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu

golang disassembly

# golang binary disassembly
go tool objdump -S exec_file_path

# 위 방식은 수백메가 이상의 파일을 생성하며 보기도 쉽지 않다.
# lensm 을 사용해보자.
# 설치
go install loov.dev/lensm@main

# 사용하기
# -watch  auto reload executable
# -filter filter the functions by regexp
# main(hello_world.go) 바이너리 assembly 보기
lensm -watch -filter main main

# 요렇게 창이 뜨고 소스 라인 별로 어셈블리 코드를 편하게 확인할 수 있다.

golang package name

# golang 에서 package(module)명은 지을 때 소문자로 모두 붙여쓰라고 한다.
# camel_case, snake_case 등은 사용하지 말고
computeServiceClient
priority_queue

# 다음과 같이 소문자로만 의미있게 축약하는것이 좋다고 한다.
strconv (string conversion)
syscall (system call)
fmt (formatted I/O)

# k8s 소스에서 package 를 검색해봤다.(정규식에서 길이 조건을 쓸수 없다.)
# syscall 처럼 의미 있게 축약되면 좋지만 client, pod 이런 단어가 조합되는 경우 축약이 힘들어 보인다.

# k8s 소스를 다운받아 package 이름 긴것들을 확인해 보면 이렇다.
# 소문자까지는 좋은데 넘 길면 갠적으로 snake_case 가 더 눈에 잘 들어온다.
rg -IN "^package [[:alpha:]]{10,50}$" | awk '{print length, $0}' | sort -r | uniq | head -10
40 package validatingadmissionpolicybinding
39 package validatingadmissionpolicystatus
38 package validatingwebhookconfiguration
37 package prereleaselifecyclegenerators
37 package externalaccountauthorizeduser
36 package storageobjectinuseprotection
36 package mutatingwebhookconfiguration
34 package prioritylevelconfiguration
34 package extendedresourcetoleration
34 package clusterauthenticationtrust

keep swag fmt

# .go 파일에서 swag comment 작성 후 swag fmt 하면 다음과 같이 주석을 보기 좋게 간격을 띄워 준다.

# 그런데 vim, vscode 등에서 저장하면 gofmt 이 동작하며 다음과 같이 주석 앞쪽을 공백으로 바꾸면서 간격이 흐트러진다.
# 이상태에선 swag fmt 해도 포맷팅이 되지 않는다.
# 이상태에서 탭 부분을 수정 후 swag fmt 하면 포맷팅돼 위 화면처럼 된다.

# vim 등에서 저장(gofmt 으로 주석이 포맷팅)시 swag fmt 부분을 유지하려면
# 다음과 같이 맨위 주석과 공백 주석을 추가하면 gofmt 해도 이 swag 주석 부분이 변경되지 않는다.

#####

# default, enum 등의 속성 사용 예시
// @Param   enumstring  query     string     false  "string enums"       Enums(A, B, C)
// @Param   enumint     query     int        false  "int enums"          Enums(1, 2, 3)
// @Param   enumnumber  query     number     false  "int enums"          Enums(1.1, 1.2, 1.3)
// @Param   string      query     string     false  "string valid"       minlength(5)  maxlength(10)
// @Param   int         query     int        false  "int valid"          minimum(1)    maximum(10)
// @Param   default     query     string     false  "string default"     default(A)
// @Param   example     query     string     false  "string example"     example(string)
// @Param   collection  query     []string   false  "string collection"  collectionFormat(multi)
// @Param   extensions  query     []string   false  "string collection"  extensions(x-example=test,x-nullable)

golangci-lint

# vim 에서 golang generic(1.18)를 사용하는데 lint 에러가 많이 발생한다.
# vim > vim-go > golint 를 사용하고 있고 golint 바이너리는 업데이트 해도 다음과 같은 메시지가 발생한다.
golint ./...
main.go:12:8: expected ';', found '|' (and 4 more errors)

# vim > ale > gofmt 도 다음과 같은 에러 메시지를 발생한다.
gofmt .
main.go:12:8: expected ';', found '|'
main.go:12:10: illegal character U+007E '~'
main.go:16:2: expected '}', found 'return'
main.go:20:2: expected declaration, found result

https://github.com/golang/lint 가보니 2021년에 deprecated 돼 관리가 안되고 있었다.
# gofmt 바이너리는 2018년에 생성된것을 사용하고 있었다.
type gofmt
gofmt is /Users/ysoftman/workspace/gopath/bin/gofmt --> 2018년도
gofmt is /opt/homebrew/bin/gofmt --> ../Cellar/go/1.22.1/bin/gofmt

# golint 는 삭제하고
rm -f $(which golint)

# 2018년도 gofmt 삭제
rm -f /Users/ysoftman/workspace/gopath/bin/gofmt

# 대안으로 https://github.com/golangci/golangci-lint 를 사용하면 된다.
# 설치
brew install golangci-lint

# 실행
golangci-lint run ./...

# revive 사용시 설치
go install github.com/mgechev/revive@latest

# vim-go 사용시
":GoMetaLinter 명령실행시 동작할 커맨드
let g:go_metalinter_command = "golangci-lint"
"최신 golangci-lint 에서 --deadline 옵션이 --timeout 으로 변경됨
"vim-go 에서는 아직 deadline 을 사용하고 있어 주석처리함
"let g:go_metalinter_deadline = "5s"
"golangci-lint 에서 활성화할 항목
"vet -> govet 으로 바뀜
let g:go_metalinter_enabled = ['govet', 'revive', 'errcheck']

# ale 사용시
"vim-go 와 달리 linter 커맨드를 입력하지 않아도 golangci-lint 결과가 코드에 자동으로 표시된다
let g:ale_linters = {
\ 'python': ['flake8', 'pylint'],
\ 'javascript': ['eslint'],
\ 'go': ['golangci-lint', 'gofmt']
\}


gorm join table

# 다음과 department:member 가 M:N 관계인 테이블이 있을때
[departments table]
id uint
name string

[members table]
id uint
name string

[departments_members table](관계 매핑테이블/join table)
id uint
department_key uint
member_key uint
val1 string
val2 string

# 조인 테이블은 다음과 같이 FK 를 설정했다.
`FK_MEMBERS` FOREIGN KEY (`member_key`) REFERENCES `members` (`id`)
`FK_DEPARTMENTS` FOREIGN KEY (`department_key`) REFERENCES `departments` (`id`),

# golang gorm 사용시
# 사용 테이블 이름 설정
type Tabler interface { TableName() string}
func (Department) TableName() string  { return "departments" }
func (Member) TableName() string  { return "members" }

# 부서 조회시 부서에 속한 멤버들을 조회를 한다고 했을때
# many2many 로 조인테이블 departments_members 을 참고하도록 했고
type Member struct {
  ID   uint64  `gorm:"primary_key;auto_increment" json:"id"`
  Name string  `gorm:"unique;size:100;not null" json:"name"`
  Departments []Department `gorm:"many2many:departments_members;" json:"departments,omitempty"` 
}
type Department struct {
  ID   uint64  `gorm:"primary_key;auto_increment" json:"id"`
  Name string  `gorm:"unique;size:100;not null" json:"name"`
  Members []Member `gorm:"many2many:departments_members; "json:"members,omitempty"`
}

# 다음과 preload 로 멤버가 포함된 부서를 조회를 하면 
err := ysoftmanMysql.Db.Model(&db.Department{}).Preload("Members").Find(&department, "name = ?", name).Error

# 다음과 같이 department_id 를 알 수 없다고 나온다.
Error 1054 (42S22): Unknown column 'departments_members.department_id' in 'where clause'

# 기본적으로 foreignkey로 모델명(struct)+pk 를 붙인 이름(department_id)필드를 사용한다.
# departments_members 의 필드명을 다음과 같이 바꾸면 된다.
department_key -> department_id
member_key -> member_id

# 참고
# github.com/jinzhu/gorm -> 1.x
# gorm.io/gorm -> 2.x

#####

# departments_members 테이블 자체에 있는 val1,val2 필드고 같이 조회 하기
func (DepartmentsMembers) TableName() string { return "departments_members" }

type Member struct {
  ID   uint64  `gorm:"primary_key;auto_increment" json:"id"`
  Name string  `gorm:"unique;size:100;not null" json:"name"`
  # DepartmentMembers 에서 MemberID 로 조회 할 수 있도록 한다.
  DepartmentMembers []DepartmentMembers `gorm:"foreignkey:MemberID" json:"users,omitempty"`
}

type Department struct {
  ID   uint64  `gorm:"primary_key;auto_increment" json:"id"`
  Name string  `gorm:"unique;size:100;not null" json:"name"`
  # DepartmentMembers 에서 DepartmentID 로 조회 할 수 있도록 한다.
  DepartmentMembers []DepartmentMembers `gorm:"foreignkey:DepartmentID" json:"users,omitempty"`
}

# DepartmentsMembers 데이터추가고 이곳에서 Member, Department 를 담을 수 있도록 한다.
type DepartmentsMembers struct {
  ID       uint64  `gorm:"primary_key;auto_increment" json:"id"`
  MemberID   uint64  `gorm:"";auto_increment" json:"user_id,omitempty"`
  DepartmentID uint64  `gorm:"" json:"user_id,omitempty"`
  Member      *Member  `json:"member,omitempty"`
  Department  *Department  `json:"department,omitempty"`
}

# Department 조회시 멤버 정보가 포함해서 조회
# DepartmentsMember.Member 를 한번 더 프리로드 한다.
err := ysoftmanMysql.Db.Preload("DepartmentsMember").Preload("DepartmentsMember.Member").Find(&project, "project_id = ?", id).Error

golangci-lint cpu usage

# vscode 로 golang 을 오픈하면 갑자기 컴이 버벅거린다.
# 아래 프로세스가 CPU 를 모두 사용하고 있었다.
golangci-lint run --print-issued-lines=false --out-format=colored-line-number --issues-exit-code=0

# golangci-lint 커맨드 설명을 보면 기본 16core 를 사용하고 있다.
-j, --concurrency int           Concurrency (default NumCPU) (default 16)

# vscode 설정을 요렇게 변경해보자.
// file 저장시 lint 수행, lint 결과 problems 에 표시
"go.lintOnSave": "file",
"go.lintTool": "golangci-lint",
"go.lintFlags": [
  "--allow-parallel-runners",
  "--concurrency",
  "4"
],

docker build image apt-update error

# 잘 되던 docker 이미지 빌드 중 Dockerfile 에서 패키지 업데이트시
RUN apt-get update
...
#6 10.23 Reading package lists...
#6 10.24 W: The repository 'http://security.debian.org/debian-security stretch/updates Release' does not have a Release file.
#6 10.24 W: The repository 'http://deb.debian.org/debian stretch Release' does not have a Release file.
#6 10.24 W: The repository 'http://deb.debian.org/debian stretch-updates Release' does not have a Release file.
#6 10.24 E: Failed to fetch http://security.debian.org/debian-security/dists/stretch/updates/main/binary-amd64/Packages  404  Not Found
#6 10.24 E: Failed to fetch http://deb.debian.org/debian/dists/stretch/main/binary-amd64/Packages  404  Not Found
#6 10.24 E: Failed to fetch http://deb.debian.org/debian/dists/stretch-updates/main/binary-amd64/Packages  404  Not Found
#6 10.24 E: Some index files failed to download. They have been ignored, or old ones used instead.
------
executor failed running [/bin/sh -c apt-get update && apt-get install -y net-tools htop lsof wget curl rsync vim tar man-db traceroute]: exit code: 100

# 기본 이미지를 변경하면 잘 동작한다. 
FROM golang:1.17-stretch  -> FROM golang:1.19

# 참고로 데이안 계열 이미지 suffix 로 붙는 이름(코드네임)
-없는경우 debian latest
-bullseye: debian 11 
-buster: debian 10.4
-stretch: debian 9
-jessie: debian 8

mongodb-go-driver cursor not found error

# mongo go client v1.9.1 (https://github.com/mongodb/mongo-go-driver) 로 documents 조회(find)를 다음과 같이 구현했다.
cursor, err := c.collection.Find(context.TODO(), filter)
if err != nil {
    return err
}
defer cursor.Close(context.TODO())
return cursor.All(context.TODO(),  results)

# 여러개의 조회 요청이 동시에 들어오면 일부 요청 결과에 다음 에러가 발생했다.
(CursorNotFound) Cursor not found (namespace:

# 에러를 발생하는 mongo-go-driver 부분
# https://github.com/mongodb/mongo-go-driver/blob/4f06ad2489b73cd1dbcebb5e7df09b77cb643be1/mongo/cursor.go#L266

# document 수가 50개로 적은 컬렉션에 대해서 조회시 발생하지 않고,
# document 수가 156개인 컬렉션에서 발생했다.
 
# 커서를 읽기(cursor.All)전에 현재 커서에 있는 document 를 찍어보면 101로 보인다.
cursor.RemainingBatchLength()

# 이유는 default batchSize=101 이기 때문이다.
# 인덱스 없는 정렬시에는 전체 document 를 로딩한다고 한다.
```
find() and aggregate() operations have an initial batch size of 101 documents by default. Subsequent getMore operations issued against the resulting cursor have no default batch size, so they are limited only by the 16 megabyte message size.

For queries that include a sort operation without an index, the server must load all the documents in memory to perform the sort before returning any results.
```

# Find 옵션으로 batch 크기를 다음과 같이 설정했다.
opts := options.Find()
opts = opts.SetBatchSize(500)
cursor, err := c.collection.Find(context.TODO(), filter, opts)

# cursor.RemainingBatchLength()를 출력해보면 156 이다.
# 위와 같이 한번에 전체 documents 를 가져오니 cursor not found 에러가 발생하지 않았다.
# batchSize 가 전체 documents 보다 작으면 계속 batchSize 만큼 조회해서 가져오는것으로 보인다.(실제 batchSize 101일때 1번 find 최종 조회 결과는 156개의데이터를 모두 가져왔다.)
# 하지만 이 과정에서 동시에 조회가 실행될 경우 서버로 부터 커서를 찾을 수 없다는 응답(헤더)를 받는것으로 보인다.

# 참고
# default batchSize 로 인한 지연
# batchSize 설정값에 따른 성능 결과

add timezone in alpine

# golang 에서 타임존이 변경된 time 사용을 위해 time.LoadLocation 를 사용했다.
loc, _ := time.LoadLocation("Asia/Seoul")
t = t.In(loc)

# 로컬에서는 동작했지만,
# 배포된 도커에서는 loc 를 찾을 수 없어 panic 이 발생하고 있었다.
[PANIC RECOVER] time: missing Location in call to Time.

# 컨테이너 사이즈를 줄이기 위해 alpine 이미지를 사용하고 있었는데,
# LoadLocation 가 참조하는 zoneinfo 데이터가 존재하지 않아 발생한 문제였다.

# dockerfile 에 tzdata 를 설치를 추가하자.
RUN apk add tzdata

# 이제 다음과 같은 위치에 timezone 데이터 값이 추가되고,
# time.LoadLocation 도 사용할 수 있다.
/usr/share/zoneinfo/Asia/Seoul

golang vim-go GOBIN error

# go 1.19.2 버전 설치 후 .go 파일을 열때 다음과 같은 에러가 발생했다.
# :messages 로 확인해보면
vim-go: 'go env GOBIN' failed
vim-go: could not determine appropriate working directory for gopls
FileNotFoundError: [Errno 2] No such file or directory: '/Users/ysoftman/.vim/plugged/youcompleteme/third_party/ycmd/third_party/go/bin/gopls'

# vim plug 를 업데이트해도 에러가 발생한다.
:PlugUpdate

# 참고로 vim-go 최신 소스를 보면 go env gobin > $GOBIN > GOPATH./bin 순으로 찾고 있다.

# 다음과 같이 설정해도 에러가 발생한다.
# GOBIN (go install 시 실행 binary 설치 경로)
go env -w GOBIN=/Users/ysoftman/workspace/gopath/gopath/bin
go env GOBIN

# 원인은 go 실행 자체가 안되는것이 문제였다.
# go 신규 버전 설치시 macOS(darwin)환경인데 linux 바이너리파일을 설치한것이 문제였다.
# go1.19.2.linux-amd64.tar.gz -> go1.19.2.darwin-amd64.tar.gz 로 변경해서 설치해고 go 가 실행된다.
# 이제 GOBIN 을 설정하지 않아도(GOPATH/bin 로 찾는다.) 에러가 없다.
go env -w GOBIN=

# vim-plug 라면 go binary 업데이트도 해주자.
:GoUpdateBinaries

# ycmd 에러 관련해서는 다음과 같이 설치하면 된다.
cd ~/.vim/plugged/youcompleteme/third_party/ycmd/
git checkout master
git pull
git submodule update --init --recursive
./build.py --go-completer

mongodb find pagination

# mongodb collection 의 document 조회(find)시 pagination
// document _id 다음 포맷으로 구성되어 있어, _id 크기비교로 범위를 탐색할 수 있다.
// https://www.mongodb.com/docs/manual/reference/method/ObjectId/#objectid
// A 4-byte timestamp + A 5-byte random value + A 3-byte incrementing counter
// 가장 오래된 document 파악
filter = bson.D{{Key: "number", Value: bson.D{{Key: "$gt", Value: 0}}}}
docs = make([]samepleDoc1, 0)
// 가장 오래된 document 부터 2개 문서 가져오기
c.FindByPageSize(filter, &docs, 2)
page := 0
lastObjID := docs[len(docs)-1].ObjId
PrintDoc(docs)
page++
fmt.Printf("lastObjID %v page %v\n", lastObjID, page)
// 페이지당 2개 documents 로 계속 조회
for len(docs) > 0 {
lastObjID := docs[len(docs)-1].ObjId
filter = bson.D{{Key: "_id", Value: bson.D{{Key: "$gt", Value: lastObjID}}}}
docs = make([]samepleDoc1, 0)
c.FindByPageSize(filter, &docs, 2)
PrintDoc(docs)
page++
fmt.Printf("lastObjID %v page %v\n", lastObjID, page)
}

# 조회시 위 설정한 objid(_id) 크기로 비교로 다음 document를 조회한다.
func (mc *MongoDBClient) FindByPageSize(filter interface{}, r interface{}, pageSize int) {
// ascending sort by document _id
opt1 := options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).SetLimit(int64(pageSize))
cursor, err := mc.Client.Database("my_db").Collection("my_coll").Find(context.TODO(), filter, opt1)
if err != nil {
log.Println("failed to find document", err.Error())
}
defer cursor.Close(context.TODO())
// decode each document into r
if err := cursor.All(context.TODO(), r); err != nil {
log.Println("failed to decode document", err.Error())
}
log.Println("---Find result---")
}

# 테스트 코드
# https://github.com/ysoftman/test_code/tree/master/mongodb_golang

echo reverse proxy ingress localhost 404 error

/*
echo golang web framework 의 proxy 사용시 upstream 을 ingress domain 을 사용하면 404 not found 응답 이슈가 발생했다.

echo proxy 사용 예시 참고
https://echo.labstack.com/cookbook/reverse-proxy/

같은 localhost 로 업스트림 설정하면 문제가 없다.
http://localhost:8080 -> http://localhost:8081 (upstream local)

ingress domain 으로 업스트림을 설정하면 404 응답을 받는다.
http://localhost:8080 -> http://ysoftman.dev:8081 (upstream ingress)
404 Not Found

nginx pod 에 로그를 보면 다음과 같은 메시지가 기록된다.
호스트가 ysoftman.dev 로 되어야 할것 같은데, localhost 로 파악돼 pod 까지 요청이 전달되지 않는다.
Skipping metric for host not being served" host="localhost"

해결 방법
프록시 설정전 다음과 같이 핸들러를 등록하고 request host 를 ysoftman.dev 로 설정하면 ysoftman.dev 로 부터 200 OK 응답을 받는다.
*/

package main

import (
"net/http"
"net/url"

// ModifyResponse 사용을 위해선 echo v4 가 필요
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
url, _ := url.Parse("http://ysoftman.dev:8081")
e := echo.New()
g := e.Group("/test")

// 프록시 설정 전 request host 를 upstream host 로 변경
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Request().Host = url.Host
return next(c)
}
})

// set proxy upstream
proxyTargets := []*middleware.ProxyTarget{
{
URL: url,
},
}
g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{
Balancer: middleware.NewRoundRobinBalancer(proxyTargets),
ModifyResponse: func(resp *http.Response) error {
return nil
},
}))
}

golang xor bitwise 사용하기

// golang 에는 not 비트연산자(C 에서는 ~)가 없어 XOR 를 이용해 구한다.
// XOR 를 단항연산으로 사용시 보수(not)를 구할 수 있다.
// https://go.dev/ref/spec#Arithmetic_operators
// +x                          is 0 + x
// -x    negation              is 0 - x
// ^x    bitwise complement    is m ^ x  with m = "all bits set to 1" for unsigned x
//                                       and  m = -1 for signed x
// 위 설명대로 ^x 는
// x 가 unsigned 라면 0xfffff..(모든비트1) ^ x 로 동작하고
// x 가 signed 라면 -1 ^ x 로 동작한다

// not 구하기
var aa uint = 5 // 0101
fmt.Printf("%3d(%10b)\n", aa, aa)
// 모든 비트가 1로 설정된 m 과 xor 되어 not(비트 반전, 1의보수)결과를 구할 수 있다.
fmt.Printf("%3d(%10b)\n", ^aa, ^aa)
// 0xf 등으로 xor 해서 비트 범위를 줄여서 not 결과를 만들 수도 있다.
fmt.Printf("%3d(%10b)\n", 0xf^aa, 0xf^aa)

bb := 5 // 0101
// singed 형이라면 위 설명대로 -1 ^ bb 가 된다.
fmt.Printf("%3d(%10b)\n", ^bb, ^bb)

// 양수 -> 음수, MSB(Most Significant Bit) 는 음수가 된다.
// bb 는 signed 라서 위 설명처럼 ^bb 하면 -1 과 xor 된다.
// 여기에 +1(2의보수)를 하면 음수가 된다.
fmt.Printf("%3d(%8b) -> %3d(%8b) -> %3d(%8b)\n", bb, bb, ^bb, ^bb, ^bb+1, ^bb+1)

bb = -5
// 음수 -> 양수, MSB(Most Significant Bit) 는 양수가 된다.
// bb 는 signed 라서 위 설명처럼 ^bb 하면 -1 과 xor 된다.
// 여기에 +1(2의보수)를 하면 양수가 된다.
fmt.Printf("%3d(%8b) -> %3d(%8b) -> %3d(%8b)\n", bb, bb, ^bb, ^bb, ^bb+1, ^bb+1)

# 참고

golang filename rules

# golang 파일을 생성할때 파일리스트 특정 파일을 가장 위쪽에 보려고 
# _aaa.go 와 같이 _로 시작하는 파일을 만들고 go build(run) 하면
# 다음과 같이 파일을 찾지 못한다.
package command-line-arguments: no Go files in /xxxx

# 찾아보니 .go .c 등의 파일은 _ 나 . 로 시작하면 대상에서 제외된다.
// Import returns details about the Go package named by the import path,
// interpreting local import paths relative to the srcDir directory.
// If the path is a local import path naming a package that can be imported
// using a standard import path, the returned package will set p.ImportPath
// to that path.
//
// In the directory containing the package, .go, .c, .h, and .s files are
// considered part of the package except for:
//
// - .go files in package documentation
// - files starting with _ or . (likely editor temporary files)
// - files with build constraints not satisfied by the context
//
# 출처

# 추가로 패키지 이름은 _ 나 대소문자구분 없이 모두 소문자로 사용한다.
Good package names are short and clear. They are lower case, with no under_scores or mixedCaps.

k8s deployment or pod serialization

# golang 환경에서 k8s deployment(k8s.io/api/apps/v1 -> Deployment struct) 를 manifest(yaml)로 serialization 할때
# 일반적인 gopkg.in/yaml.v2 패키지를 사용시
yamlDeployment, err := yaml.Marshal(deployment)

# string(yamlDeployment) 결과를 보면
# 다음과 같이 cpu 리소스등의 값이 DecimalSI 포맷이라고만 나오고 정확한 값이 표시되지 않는다.
spec -> template -> spec -> containers -> resources
  limits:
    cpu:
      format: DecimalSI
  requests:
    cpu:
      format: DecimalSI

# pod 나 deployment 는 k8s apimachinery 패키지의 NewYAMLSerializer 를 사용하면 된다.
import k8sJson "k8s.io/apimachinery/pkg/runtime/serializer/json"

e := k8sJson.NewYAMLSerializer(k8sJson.DefaultMetaFactory, nil, nil)
yamlDeployment := new(bytes.Buffer)
err = e.Encode(deployment, yamlDeployment)

# yamlDeployment.String() 결과를 보면 다음과 같이 값이 잘 표시된다.
  limits:
    cpu: 2500m
  requests:
    cpu: "1"

golang chi 사용시 높은 CPU 사용율

# golang chi 웹프레임워크 기반 웹서버 성능 테스트를 했다.
# 참고로 jplot 까지 보려면 iterm2(tmux 사용하지 않고)에서 실행해야 한다.
echo 'GET https://localhost/version' | \
vegeta attack -rate=2000/1s -workers=100 -duration 60s --insecure | vegeta encode | \
jaggr @count=rps \
      hist\[100,200,300,400,500\]:code \
      p25,p50,p95:latency \
      sum:bytes_in \
      sum:bytes_out | \
jplot rps+code.hist.100+code.hist.200+code.hist.300+code.hist.400+code.hist.500 \
      latency.p95+latency.p50+latency.p25 \
      bytes_in.sum+bytes_out.sum

# 아주 간단한 정보 요청에 대해 너무 많은 CPU 가 사용되고 있었다.

# 특이한 점은 content-type: application/json 헤더를 설정한 api에서 발생한다.

# 스트레스 테스트 돌리는 중에 30초동안 프로파일링 덤프 받고
curl -k 'https://localhost/debug/pprof/profile?seconds=30' -o z.out 
# 로컬 브라우저로 띄워 보기
go tool pprof -http=:9999 z.out
# view -> top 을 보면 compress 부분이 보인다.

# 코드에 보니 다음과 같이 chi middleware compress 를 사용한다.
import "github.com/go-chi/chi/middleware"
... 생략 ...
r := chi.NewRouter()
r.Use(middleware.DefaultCompress)

# middleware.DefaultCompress 를 활성화하면 json 과 같은 몇몇 디폴트 content-type 에 대해 압축을 시도하게 되고 이때 많은 CPU 를 사용한다.
w.Header().Set("Content-Type", application/json; charset=UTF-8) 

# middleware.DefaultCompress 제거후 cpu 사용률이 1/4 이상 줄었다.

golang 1.16 go.sum 에러

# go.sum 은 go.mod 에 명시된 패키지들을 다운로드 후 각 패키지를 해시값으로 표시해
# 추후 로컬 패키지가 정상인지(변조,오염되지는 않았는지) 파악하는 체크썸 파일이다.
# 가끔 go.sum 을 커밋을 하지 않는(.gitignore 에 명시해서) 경우가 있는데
# go.sum 을 커밋해 다른 동료들과 패키지 버전을 맞출 수 있도록 해야 한다.

# 빌드는 되지만 vscode 에 다음과 같은 에러 메시지가 보였다.

error while importing github.com/stretchr/testify/assert: missing go.sum entry for module providing package github.com/pmezard/go-difflib/difflib (imported by github.com/stretchr/testify/assert); to add:
go get github.com/stretchr/testify/assert@v1.6.1compiler

missing go.sum entry for module providing package github.com/tidwall/match (imported by github.com/tidwall/gjson); to add:
        go get github.com/tidwall/gjson@v1.6.0

# 필요없는 패키지를 정리(tidy)해도 에러 메시지가 발생한다.
go mod tidy

# 원인은 go 1.13 버전을 사용하다 1.16 버전으로 명시된 go.mod 를 사용해서다.
# go.sum 체크썸 확인해보니 내 로컬의 mod cache(GOPATH/pkg/mod)가 go 1.13 으로 되어 있어 문제가 됐다.
# vscode IDE 에서 go.sum 기반으로 자동으로 잘못된 부분을 에러로 표시하고 있었다.

# 현재 go 1.16 임을 확인하고
go version
go version go1.16 darwin/amd64

# 다음 명령으로 다운로드된 패키지로 로컬 캐시를 업데이트 하면 된다.
go mod download

# 참고로
# go 1.15 까지는 -mod=mod 가 디폴트였지만
# go 1.16 부터 -mod=readonly 가 디폴트로, go.mod, go.sum 를 업데이트를 위해선
# 명시적으로 (go build, go test 등 사용시) -mod=mod 를 명시해야 한다.

js fetch() with credntial 옵션 사용시 CORS 에러 방지 서버 설정

# javascript(js) fetch 함수 요청시 CORS(Cross-Origin Resource Sharing) 에러 방지하기
# js fetch() 로 ysoftman1 에서 ysoftman2 로 요청하는 상황으로 다음과 같다.
# 크롬 개발자 도구 console 에서 실행
fetch("http://ysoftman2/lemon", {
  "headers": {
    "accept": "application/json, text/plain, */*",
    "accept-language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
    "authorization": "Bearer aaabbbccc123456789",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-site"
  },
  "referrer": "https://ysoftman1/apple",
  "referrerPolicy": "no-referrer-when-downgrade",
  "body": null,
  "method": "GET",
  "mode": "cors",
  "credentials": "include"
}).then(response => response.json());

# 이때 요청 흐름은 js fetch() --> browser --> server 이 된다.
# browser 는 OPTIONS 메소드로 서버에게 접근 가능한지 물어본다.(preflight 과정)
# 이때 서버는 응답 헤더에 다음 값들을 설정할 수 있고
Access-Control-Allow-Origin: 접근 가능한 호스트
Access-Control-Allow-Methods: 접근 가능한 메소드
Access-Control-Allow-Headers: 접근 가능한 헤더

# browser 는 이 허용된 값들 내에서 서버에 실제 요청을 하게 된다.

# 서버가 와일드카드(Access-Control-Allow-Origin: *)를 사용하더라도 
# 클라가 credential (ajax, xmlhttp 로 다른도메인에 쿠키를 설정할때 사용하는 옵션)를 사용하면 CORS 정책으로 블럭된다.
# 다음과 같이 exact 한 호스명과 vary (브라우저가 캐시 사용시 어떤 헤더를 보고 구분해야 하는지 알려준다) 헤더에 origin 을 설정해야 한다.
Access-Control-Allow-Origin: https://developer.mozilla.org
Vary: Origin

# 서버쪽 설정 방법들
# 방법1 - k8s ingress 사용시, enable-cors true 설정(디폴트 false)
# enable-cors 를 사용하면 cors 관련 응답헤더들(Access-control-xxx)이 자동으로 추가된다.
# 그리고 Access-Control-Allow-Origin 가 * 로 고정된다.
# configuration-snippet 의 Access-Control-xxx 헤더가 응답에 중복되어
# cors 가 동작하지 않으니 같이 사용하지 않는다.
metadata
  annotations:
    ingress.kubernetes.io/rewrite-target: /
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/cors-allow-origin: "*"
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-headers: "x-test-header"
    
# 방법2 - k8s ingress 사용시, configuration-snippet 로 설정
metadata
  annotations:
    ingress.kubernetes.io/rewrite-target: /
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/cors-allow-origin: "*"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    # configuration-snippet 현재 location 의 설정에 추가된다.
    nginx.ingress.kubernetes.io/configuration-snippet: |
      if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Credentials' "true";
        add_header 'Access-Control-Allow-Methods' "GET, PUT, POST, DELETE, PATCH, OPTIONS";
        add_header 'Access-Control-Allow-Headers' "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Client-Identifier";
        add_header 'Access-Control-Max-Age' "1728000";
        add_header 'Content-Type' "text/plain charset=UTF-8";
        add_header 'Content-Length' "0";
        add_header 'Vary' "Origin";
        return 204;
      }
      add_header 'Access-Control-Allow-Origin' "$http_origin";
      add_header 'Access-Control-Allow-Credentials' "true";
      add_header 'Access-Control-Allow-Methods' "GET, PUT, POST, DELETE, PATCH, OPTIONS";
      add_header 'Access-Control-Allow-Headers' "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Client-Identifier";
      add_header 'Vary' "Origin";

# 방법3 - k8s ingress 사용시, 특정 도메인에만 cors 허용할때 변수를 설정해 구분
metadata
  annotations
    nginx.ingress.kubernetes.io/configuration-snippet: |
      if ($http_origin ~* "^https?:\/\/(.*\.)?((ysoftman\-lemon\.com)|(ysoftman\-apple\.com))$") {
        set $cors = "cors";
      }
      if ($request_method = 'OPTIONS') {
        set $cors = "cors_options";
      }
      if ($cors = "cors_options") {
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Credentials' "true";
        add_header 'Access-Control-Allow-Methods' "GET, PUT, POST, DELETE, PATCH, OPTIONS";
        add_header 'Access-Control-Allow-Headers' "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Client-Identifier";
        return 200;
      }
      if ($cors = "cors") {
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Credentials' "true";
        add_header 'Access-Control-Allow-Methods' "GET, PUT, POST, DELETE, PATCH, OPTIONS";
        add_header 'Access-Control-Allow-Headers' "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Client-Identifier";
        return 204;
      }

# 방법4 - 서버에 직접 설정하는 경우
# 아래는 go chi 핸들러 사용하는 예시
func CORSHandler(next http.Handler) http.Handler {
  fn := func(w http.ResponseWriter, r *http.Request) {
    // CORS(Cross-Origin Resource Sharing) 에러 방지 헤더 추가
    if r.Method == http.MethodOptions {
      // w.Header().Set("Access-Control-Allow-Origin", "*")
      // js fetch() credentials 옵션 사용시 와일드카드(*) 대신 요청 origin 으로 설정해야 한다.
      w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
      w.Header().Set("Access-Control-Allow-Credentials", "true")
      w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
      w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
      // options (preflight) 일때는 CORS 에러 방지 헤더 추가하여 바로 응답을 줘 클라가 다시 요청을 시도할 수 있도록 한다.
      return
    }
    w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
    w.Header().Set("Access-Control-Allow-Credentials", "true")
    w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
    w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
    next.ServeHTTP(w, r)
  }
  return http.HandlerFunc(fn)
}

# 주의사항
# 만약 k8s ingress 과 서버내 양쪽에서 모두 access-controll-allow 헤더들을 설정하면 중복 설정되고 CORS 정책에 위배돼 에러가 발생한다.
The 'Access-Control-Allow-Origin' header contains multiple values 'https://....', 
but only one is allowed. 
Have the server send the header with a valid value, or, 
if an opaque response serves your needs, 
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

# 그래서 한쪽에서만 설정해줘야 한다.
# 참고로 curl 로 cors 확인(access-control-xxx 헤더)을 위해선 다음 옵션을 사용해야 한다.
curl -X GET "https://ysoftman.lemon.com/aaa/bbb?ccc=lemon" \
-H "accept: application/json" \
--header 'Origin: http://ysoftman.lemon.com' \
--header 'Access-Control-Request-Headers: Origin, Accept, Content-Type' \
--header 'Access-Control-Request-Method: GET'

drone ci 사용하기

# golang 으로 만들고 docker 환경으로 실행되는 drone ci 를 사용해보자.

# 우선 로컬에서 테스트하기 때문에 
# (reverse proxy 로 ngrok 도메인 -> localhost 로 터널링 해주는) ngrok 설치
brew install ngrok

# http://fe9b41d98488.ngrok.io -> http://localhost:80 포워딩을 위해 실행
# 참고 https (tls) 터널링은 유료 버전만 가능하고, 그외 동시접속 제한이 있다.
ngrok http 80

# github oauth 로 drone 앱(서버)를 등록
# github -> settings -> developers -> oauth apps 에서 신규 생성한다.
# homepage url : http://dd441e3e9f16.ngrok.io
# callback url : http://dd441e3e9f16.ngrok.io/login

# oauth app 생성후
# client -> DRONE_GITHUB_CLIENT_ID
# client secret -> DRONE_GITHUB_CLIENT_SECRET 로 사용한다.
# generate a new client secret(생성후 한번 볼 수 있어 복붙해둬야 한다) -> DRONE_GITHUB_CLIENT_SECRET 값으로 사용

# drone <-> runner 간 RPC 통신을 위한 secret 생성 -> DRONE_RPC_SECRET 값으로 사용
openssl rand -hex 16                         
d087c94b8155367b1982238df930c2f2

# drone 도커 이미지 다운로드
docker pull drone/drone:1

# 실행
# github enterprise 는 깃헙 서버 환경 변수 설정 필요
# --env=DRONE_GITHUB_SERVER=https://github.ysoftman.com \
# 볼륨 연결로 /data/database.sqlite -> 호스트의 /var/lib/drone/database.sqlite 로 저장
docker run \
  --volume=/var/lib/drone:/data \
  --env=DRONE_DATABASE_DRIVER=sqlite3 \
  --env=DRONE_DATABASE_DATASOURCE=/data/database.sqlite \
  --env=DRONE_GITHUB_CLIENT_ID=깃헙_클라이언트_id \
  --env=DRONE_GITHUB_CLIENT_SECRET=깃헙_클라이언트_secret \
  --env=DRONE_RPC_SECRET=d087c94b8155367b1982238df930c2f2 \
  --env=DRONE_SERVER_HOST=127.0.0.1 \
  --env=DRONE_SERVER_PROTO=http \
  --env=DRONE_WEBHOOK_ENDPOINT=http://dd441e3e9f16.ngrok.io/hook \
  --env=DRONE_WEBHOOK_SECRET=d087c94b8155367b1982238df930c2f2 \
  --env=DRONE_REPOSITORY_FILTER=ysoftman,bill \
  --publish=80:80 \
  --publish=443:443 \
  --restart=always \
  --detach=true \
  --name=drone \
  drone/drone:1

# 이제 다음 url 로 github authorize 하면 깃헙으로 로그인상태로 drone ci 를 사용할 수 있다.
http://dd441e3e9f16.ngrok.io

# DRONE_REPOSITORY_FILTER 으로 특정 저장소에만 drone과 연결 할 수 있다.
# 설정 변경 후 사용자별로 로그아웃 후 다시 로그인 해야 반영된다.

#####

# 상황에 맞는 여러 runner(러너)가 있다.
# docker(임시 컨테이너를 생성해 실행할때 사용) 러너 설치
docker pull drone/drone-runner-docker:1
docker run -d \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e DRONE_RPC_PROTO=http \
  -e DRONE_RPC_HOST=dd441e3e9f16.ngrok.io \
  -e DRONE_RPC_SECRET=d087c94b8155367b1982238df930c2f \
  -e DRONE_RUNNER_CAPACITY=2 \
  -e DRONE_RUNNER_NAME=${HOSTNAME} \
  -e DRONE_DEBUG=true \
  -e DRONE_TRACE=true \
  -p 3000:3000 \
  --restart always \
  --name runner \
  drone/drone-runner-docker:1

# exec(docker 와 같은 고립된 환경이 아닌 drone 서버 장비에서 쉘 환경에서 명령을 실행할 때 사용) 러너를 설치
curl --proxy "필요한 경우 프록시 설정" -L https://github.com/drone-runners/drone-runner-exec/releases/latest/download/drone_runner_exec_linux_amd64.tar.gz | tar zx
sudo install -t /usr/local/bin drone-runner-exec
mkdir -p /home/ysoftman/.drone-runner-exec
touch /home/ysoftman/.drone-runner-exec/config
cat > /etc/drone-runner-exec/config << eof
DRONE_RPC_PROTO=http
DRONE_RPC_HOST=dd441e3e9f16.ngrok.io
DRONE_RPC_SECRET=d087c94b8155367b1982238df930c2f
DRONE_LOG_FILE=/home/ysoftman/.drone-runner-exec/log.txt
eof
drone-runner-exec service install
drone-runner-exec service start

# drone-runner-exec 서비스 상태 확인
systemctl status drone-runner-exec.service

# docker 환경 변수 확인
docker inspect -f "{{.Config.Env}}" drone  | tr " " "\n"


# ssh(ssh 로 서버에 접속해 명령을 실행하는 용도) 러너 설치
docker pull drone/drone-runner-ssh
docker run -d \
  -e DRONE_RPC_PROTO=http \
  -e DRONE_RPC_HOST=dd441e3e9f16.ngrok.io \
  -e DRONE_RPC_SECRET=d087c94b8155367b1982238df930c2f \
  -p 3000:3000 \
  --restart always \
  --name runner \
  drone/drone-runner-ssh

# 루트 경로에 .drone.yml 를 작성한다.
# 파일명은 drone ci settings 에서 변경 가능하다.
---
kind: pipeline
type: exec
name: default

platform:
  os: linux
  arch: amd64

trigger:
  event:
    - push
steps:
  - name: 작업1
    commands:
      - echo hello world1
  - name: 작업2
    commands:
      - echo hello world2
  - name: 슬랙 알림
    # environment:
    #   noti_msg: "Branch: ${DRONE_BRANCH}\nAuthor: ${DRONE_COMMIT_AUTHOR}\nLink: ${DRONE_COMMIT_LINK}"
    #   build_status: "✅ build ${DRONE_BUILD_NUMBER} succeeded. Good job."
    commands:
      - echo "파이프 종료"
      - export HTTP_PROXY="http://프록시서버"
      - export noti_msg="Branch ${DRONE_BRANCH}\nAuthor ${DRONE_COMMIT_AUTHOR}\nLink ${DRONE_COMMIT_LINK}"
      - export build_status="✅ build ${DRONE_BUILD_NUMBER} succeeded. Good job."
      - echo $DRONE_BUILD_STATUS
      - if [[ $DRONE_BUILD_STATUS == "failure" ]]; then build_status="❌ build ${DRONE_BUILD_NUMBER} failed. Fix me please."; fi
      - |
        curl https://hooks.slack.com/services/..... -d "payload={\"channel\": \"#billtest\", \"text\": \"파이프 라인 시작\n$noti_msg\n$build_status\"}"
# ---
# kind: pipeline
# type: ssh
# name: default
# # 접속할 대상 서버
# server:
#   host: ysoftman-server.com
#   user: deploy
#   password:
#     from_secret: password
# steps:
#   - name: 작업1
#     commands:
#       - echo hello world1
#   - name: 작업2
#     commands:
#       - echo hello world2
# ---
# kind: pipeline
# type: docker
# name: slack notification
# # 사내환경등에서 외부 접속이 안되는 경우 프록시 환경 변수 설정
# environment:
#  HTTP_PROXY: "http://프록시주소"
#  HTTPS_PROXY: "http://프록시주소"
# steps:
#   - name: 슬랙 알림
#     # 사내환경등에서 외부 이미지 다운로드가 안되는 경우 알맞게 변경
#     image: plugins/slack
#     settings:
#       # https://my.slack.com/services/new/incoming-webhook 참고
#       webhook: https://hooks.slack.com/services/.....
#       channel: ysoftman-test
#       template: >
#         {{#success build.status}}
#           build {{build.number}} succeeded. Good job.
#         {{else}}
#           build {{build.number}} failed. Fix me please.
#         {{/success}}
#         Branch: {{ build.branch }}
#         Author: {{ build.author }}
#         Link: {{ build.link }}

# 이제 drone DRONE_WEBHOOK_ENDPOINT 변수로 설정된 url 을
# github 저장소 webhook url 을 추가해 push 되면 drone 에 알리도록 한다.
http://dd441e3e9f16.ngrok.io/hook

# 참고로 위의 HTTP_PROXY 와 같은 환경변수를 사용할때 
# ${HTTP_PROXY} 는 안되고 {} 를 제거한 $HTTP_PROXY 로 사용해야 한다.

#####

# drone cli 툴 사용 https://docs.drone.io/cli/install/
# 설치
brew install drone-cli

# aaaaabbbbb11111 는 drone > User Settings > token 로 확인
# 접속할 drone 서버 환경 변수 설정
export DRONE_SERVER=http://dd441e3e9f16.ngrok.io
export DRONE_TOKEN=aaaaabbbbb11111

# 접속 여부 확인
drone info

# .drone.yml 작업 수행(커밋 전에 미리 로컬에서 테스트할 수 있다.)
# drone exec 현재 type: docker 만 실행된다.
# .drone.yml 을 다음과 같이 만들고
kind: pipeline
type: docker
name: default

steps:
- name: build
  image: golang:1.13
  commands:
  - echo "aaa"

# 실행
# .drone.yml syntax 체크등에 활용할 수 있다.
drone exec