궁금해서 시작..
"Turbine 은 반드시 Eureka 서비스와 같이 돌아가기 때문에, 사용하지 않는다."
"Turbine 없이 DashBoard 를 사용하는 것은 불편함을 야기하므로 K8s 내부에 올리지 않는다."
간단한 샘플링 정도만 진행한다.
Gradle 프로젝트의 상 하 관계를 이용한다.
이 부분은 호불호가 많이 갈리는 영역이므로, 개별적으로 틀거나 멀티모듈로 하거나 둘 다 선택사항이다.
프로젝트 구조
최상위 모듈의 build.gradle
#Build.gradle
buildscript {
ext {
springBootVersion = '2.3.10.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath "io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE"
}
}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.study.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
ext {
set('springCloudVersion', "Hoxton.SR11")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
}
최상위 모듈의 setting.gradle
rootProject.name = 'openfegin'
include ':service-a'
include ':service-b'
include ':service-c'
Service-A build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'
}
bootJar{
enabled(true)
}
Service-B build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'
}
bootJar{
enabled(true)
}
service-c build.gradle
bootJar{
enabled(true)
}
dashboard build.gradle
plugins {
id 'org.springframework.boot' version '2.3.10.RELEASE'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.study.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "Hoxton.SR11")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class ServiceBApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceBApplication.class, args);
}
}
feign:
client:
config:
default:
connectionTimeout: 3000
readTimeout: 3000
loggerLevel: FULL
hystrix:
enabled: true
url:
service-a: http://localhost:8080
#Circuit Breaker
#Hystrix 는 Thread Pool 에서 관리 된다.
hystrix:
threadpool:
default:
maximum-size: 20 # Thread Poll Size => default 10
command:
defualt:
execution:
isolation:
thread:
timeout-in-milliseconds: 3000 # 3초 지연시 fallback, default 1000
metrics:
rooling-stats:
time-in-milliseconds: 100000 # 오류 감시 시간 default 10000
circuit-breaker:
request-volume-threshold: 5 # 감시 시간내의 요청 수 default 20
error-threshold-percentage: 50 # 감시 시간내의 요청 수가 해당 % 를 넘으면 OPEN default 50
sleep-window-in-milliseconds: 5000 # open 서킷 유지시간
#Actuator
management:
endpoints:
web:
exposure:
include: ["health", "info", "hystrix.stream"]
endpoint:
health:
show-details: always
server:
port: 8081
logging:
level:
com:
study:
sample:
serviceb:
feign:
ServiceAClient: debug
ribbon:
http:
client:
enabled: false
---
spring:
profiles:
active: k8s
server:
port: 8080
feign:
url:
service-a: http://service-a:8080
@RestController
@RequestMapping("/api/v1")
@Slf4j
public class TestController {
private final ServiceAClient client;
public TestController(ServiceAClient client) {
this.client = client;
}
@GetMapping("/success")
public ResponseEntity<?> feignCallSuccess(){
List<?> objects = client.success();
return ResponseEntity.ok(objects);
}
@GetMapping("/time-out")
public ResponseEntity<?> feignCallTimeout() throws InterruptedException {
List<?> objects = client.timeOut();
return ResponseEntity.badRequest().body(objects);
}
@GetMapping("/exception")
public ResponseEntity<?> feignCallException() throws InterruptedException {
List<?> objects = client.exception();
return ResponseEntity.ok(objects);
}
@GetMapping("/access/success")
public ResponseEntity<?> feignCallAntherServiceSuccess() {
List<?> objects = client.feignCallSuccess();
return ResponseEntity.ok(objects);
}
@GetMapping("/access/time-out")
public ResponseEntity<?> feignCallAntherServiceTimeout() throws InterruptedException {
List<?> objects = client.feignCallTimeOut();
return ResponseEntity.badRequest().body(objects);
}
@GetMapping("/access/exception")
public ResponseEntity<?> feignCallAntherServiceException() {
List<?> objects = client.feignCallException();
return ResponseEntity.badRequest().body(objects);
}
}
@FeignClient(url = "${feign.url.service-a}",decode404 = false,value = "service-a",fallbackFactory = ServiceAClientFallbackFactory.class)
@RequestMapping("/api/v1")
public interface ServiceAClient {
@GetMapping("/service-a/success")
List<?> success();
@GetMapping("/service-a/time-out")
List<?> timeOut() throws InterruptedException;
@GetMapping("/service-a/exception")
List<?> exception();
@GetMapping("/service-a/access/success")
List<?> feignCallSuccess();
@GetMapping("/service-a/access/time-out")
List<?> feignCallTimeOut() throws InterruptedException;
@GetMapping("/service-a/access/exception")
List<?> feignCallException();
}
@Component
@Slf4j
public class ServiceAClientFallbackFactory implements FallbackFactory<ServiceAClient> {
@Override
public ServiceAClient create(Throwable cause) {
return new ServiceAClient() {
@Override
public List<?> success() {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceA > Fail");
}
@Override
public List<?> timeOut() throws InterruptedException {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceA > Time Out");
}
@Override
public List<?> exception() {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceA > Exception Catch");
}
@Override
public List<?> feignCallSuccess() {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceA > Exception Catch");
}
@Override
public List<?> feignCallTimeOut() throws InterruptedException {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceA > Exception Catch");
}
@Override
public List<?> feignCallException() {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceA > Exception Catch");
}
};
}
}
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class ServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceAApplication.class, args);
}
}
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres?currentSchema=public
username: postgres
password: tnals12@
schema: classpath*:schema.sql
initialization-mode: always
data: classpath*:data.sql
jpa:
properties:
hibernate:
show_sql: true
format_sql: true
# generate-ddl: true
hibernate:
ddl-auto: none
open-in-view: true
logging:
level:
org.hibernate.type.descriptor.sql: trace
com:
study:
sample:
servicea:
feign:
ServiceCClient: debug
feign:
url:
service-c: http://localhost:8082
client:
config:
default:
connectionTimeout: 2000
readTimeout: 2000
loggerLevel: FULL
hystrix:
enabled: true
#Actuator
management:
endpoints:
web:
exposure:
include: ["health", "info", "hystrix.stream"]
#Circuit Breaker
#Hystrix 는 Thread Pool 에서 관리 된다.
hystrix:
threadpool:
default:
maximum-size: 100 # Thread Poll Size => default 10
command:
defualt:
execution:
isolation:
thread:
timeout-in-milliseconds: 3000 # 3초 지연시 fallback, default 1000
metrics:
rooling-stats:
time-in-milliseconds: 100000 # 오류 감시 시간 default 10000
circuit-breaker:
request-volume-threshold: 5 # 감시 시간내의 요청 수 default 20
error-threshold-percentage: 50 # 감시 시간내의 요청 수가 해당 % 를 넘으면 OPEN default 50
sleep-window-in-milliseconds: 5000 # open 서킷 유지시간
server:
port: 8080
---
spring:
profiles:
active: k8s
include: prod
feign:
url:
service-c: http://service-c:8080
@RestController
@RequestMapping("/api/v1/service-a")
@Slf4j
public class TestController {
private final ServiceCClient serviceCClient;
private final TestRepository testRepository;
public TestController(ServiceCClient serviceCClient, TestRepository testRepository) {
this.serviceCClient = serviceCClient;
this.testRepository = testRepository;
}
@GetMapping("/success")
public List<?> success() {
log.warn("=================Active Netflix OpenFeign : success=================");
return testRepository.findAll();
}
@GetMapping("/time-out")
public List<?> timeOut() throws InterruptedException {
log.warn("=================Active Netflix OpenFeign : timeOut=================");
Thread.sleep(6000);
return testRepository.findAll();
}
@GetMapping("/exception")
public List<?> exception() {
log.warn("=================Active Netflix OpenFeign=================");
throw new RuntimeException("Exception from Service-A : Fallback Active");
}
@GetMapping("/access/success")
public List<?> feignCallSuccess() {
log.warn("=================Active Netflix OpenFeign : feignCallSuccess=================");
return serviceCClient.success();
}
@GetMapping("/access/time-out")
public List<?> feignCallTimeOut() throws InterruptedException {
log.warn("=================Active Netflix OpenFeign : feignCallTimeOut=================");
return serviceCClient.timeOut();
}
@GetMapping("/access/exception")
public List<?> feignCallException() {
log.warn("=================Active Netflix OpenFeign : feignCallException=================");
return serviceCClient.exception();
}
}
@FeignClient(url = "${feign.url.service-c}",decode404 = false,value = "service-a",fallbackFactory = ServiceCClientFallbackFactory.class)
@Component
@RequestMapping("/api/v1/service-c")
public interface ServiceCClient {
@GetMapping("/success")
public List<?> success();
@GetMapping("/time-out")
public List<?> timeOut() throws InterruptedException;
@GetMapping("/exception")
public List<?> exception();
}
@Component
@Slf4j
public class ServiceCClientFallbackFactory implements FallbackFactory<ServiceCClient> {
@Override
public ServiceCClient create(Throwable cause) {
return new ServiceCClient() {
@Override
public List<?> success() {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceC > Fail");
}
@Override
public List<?> timeOut() throws InterruptedException {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceC > Time Out");
}
@Override
public List<?> exception() {
log.error("Fallback Factory Log: {}",cause.getCause());
return Collections.singletonList("ServiceC > Exception Catch");
}
};
}
}
@RestController
@RequestMapping("/api/v1/service-c")
@Slf4j
public class PrintController {
@GetMapping("/success")
public List<?> success() {
log.warn("=================Active Netflix OpenFeign : success=================");
return Collections.singletonList("Hello");
}
@GetMapping("/time-out")
public List<?> timeOut() throws InterruptedException {
log.warn("=================Active Netflix OpenFeign : timeOut=================");
Thread.sleep(6000);
return Collections.singletonList("Hello");
}
@GetMapping("/exception")
public List<?> exception() {
log.warn("=================Active Netflix OpenFeign : exception=================");
throw new RuntimeException("Exception from Service-A : Fallback Active");
}
}
@SpringBootApplication
@EnableHystrixDashboard
public class DashboardApplication {
public static void main(String[] args) {
SpringApplication.run(DashboardApplication.class, args);
}
}
server:
port: 7000
hystrix.dashboard.proxy-stream-allow-list: localhost
Kubelet 프로세스가 Master 의 Kube-Apiserver 의 요청을 받아서 [물론 RBAC 검증 후] Docker 자원량 한도를 컨테이너에 걸게 되므로 [Pod.spec.containers[] 아래에 작성하는 거 보면 분명해..]
# 이럴땐 노드로 직접 들어가서 Kubelet 프로세스가 만든 컨테이너를 까보면 되겠다.
docker inspect [id] | grep -i -A10 mem
물론, Kube-Scheduler 의 스케쥴링 filter에 Pod Profile 에도 해당 되므로 중요한 부분이라고 보면 되겠다.
FROM openjdk:12.0.2
EXPOSE 8080
ADD ./build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=k8s","/app.jar"]
docker build -t [repo]/[name]:[tag] ./service-a/
docker build -t [repo]/[name]:[tag] ./service-b/
docker build -t [repo]/[name]:[tag] ./service-c/
docker push [repo]/[name]:[tag]
docker rmi ~~~ & docker image prune
보통 이런 직접 적는 템플릿을 Local 템플릿, K8s 내부 메모리 [ETCD] 에 존재하는 [kubeclt get [resource][name] -o yaml] 템플릿은 K8sCluster 내부 템플릿 ( Kubelet 이 적어주는 .status 하위필드를 의미하고는 함), .metadata.annotations.kubectl.kubernetes.io/last-applied-configuration 의 경우 마지막으로 수정된 템플릿을 JSON 형식으로 가지고 있는데, 이 3가지가 Apply , 즉 선언적 방식에 대한 자동 프로세싱을 가능케 함
apiVersion: v1
kind: Service
metadata:
name: sample-nginx
namespace: default
spec:
type: ClusterIP
selector:
app: nginx
proj: sample
env: dev
ports:
- port: 80
targetPort: 80
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-nginx
labels:
app: nginx
proj: sample
env: dev
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
proj: sample
env: dev
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 30%
maxUnavailable: 30%
template:
metadata:
name: sample-nginx
namespace: default
labels:
app: nginx
proj: sample
env: dev
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/arch
operator: In
values:
- amd64
- arm64
containers:
- name: nginx
image: sample/sample-nginx
volumeMounts:
- mountPath: /html/
name: index
ports:
- containerPort: 80
imagePullSecrets:
- name: my-repo
volumes:
- hostPath:
path: /app/sample/html/
type: DirectoryOrCreate
name: index
생성 및 확인
...이게 확인이 그때그때마다 달라서, 로그보고 자원정보보고 해결하는게 가장 이롭다.
kubectl apply -f [name].yaml
kubectl get svc,deploy,rs,po -n default -l "proj in (sample), app in (nginx), env in (dev)" -o wide
>> 노드 스케쥴링위치, 자원 상태, 전부 체크
kubectl describe svc sample-nginx -n default | grep -A6 -i endpoint
>> 이것만 하면 안된다. 반드시 꼼꼼히 전부 체크하자.
kubectl exec -it sample-nginx-UUID-UUID bash
# cat /etc/nginx/conf.d/default.conf
# exit
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
host: [도메인]
paths:
- path: /
pathType: Prefix
backend:
service:
name: sample-nginx
port:
number: 80
생성 및 확인
kubectl apply -f sample-ingress.yaml
kubectl get ing -n default
kubectl describe ing [name] -n default
apiVersion: v1
kind: Service
metadata:
name: service-a
namespace: sample
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: service-a
proj: sample
env: dev
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-a
namespace: sample
labels:
app: service-a
proj: sample
env: dev
annotations:
kubernetes.io/change-cause: "Jenkins Build"
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 30%
maxUnavailable: 30%
replicas: 2
selectors:
matchLabels:
app: service-a
proj: sample
env: dev
template:
metadata:
name: service-a
namespace: sample
labels:
app: service-a
proj: sample
env: dev
spec:
restartPolicy: Always
terminationGracePeriodSeconds: 10
imagePullSecrets:
- name: my-repo
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- "service-a"
- key: proj
operator: In
values:
- "sample"
- key: env
operator: In
values:
- "dev"
topologyKey: "kubernetes.io/hostname"
containers:
- name: service-a
image: [repo]/service-a:latest
ports:
- containerPort: 8080
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /actuator/info
port: 8080
failureThreshold: 4
initialDelaySeconds: 7
periodSeconds: 5
timeoutSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 4
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 15
자원 생성 및 확인
# cat ~/.kube/config
kubectl config view
# 필요하다면 하나 만들자
kubectl config set-context [name] --current --namespace=[sample] --cluster=[clustername] --user=[ServiceAccount 자원 이름]
# 정신건강에 이로운 컨텍스트 변경
kubectl config use-context [name]
# 그냥 하기
kubectl get svc,deploy,po -n sample -l app=service-a,proj=sample,env=dev -o -wide
kubectl describe po -n sample [name]
> 서비스 , 디플로이먼트, 레플리카 셋 모두 확인
kubectl logs -f [pod-name] -n sample