쿠버네티스, eBPF, 그리고 머신러닝 이야기
by Yang Taehwan
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
event: notification
data: client connected
docker run --name local-redis -p 6379:6379 -d redis:7
./gradlew bootRun
./gradlew bootRun --args='--server.port=8081'
curl -X POST http://localhost:8080/notifications/broadcast \
-H "Content-Type: application/json" \
-d '{"message":"hello from 8080"}'
./gradlew bootRun
curl -N http://localhost:8080/subscribe
curl -X POST http://localhost:8080/notifications/broadcast \
-H "Content-Type: application/json" \
-d '{"message":"hello"}'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
spec:
ingressClassName: nginx
rules:
- host: sseexample.ythwork.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: sse-example
port:
number: 8080
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sse-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
ingressClassName: nginx
rules:
- host: sseexample.ythwork.com
http:
paths:
- path: /subscribe
pathType: Prefix
backend:
service:
name: sse-example
port:
number: 8080
location /api {
proxy_pass ...
}
location /subscribe {
proxy_buffering off;
proxy_read_timeout 3600;
}
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe(HttpServletResponse response) {
response.setHeader("Cache-Control", "no-cache");
response.setHeader("X-Accel-Buffering", "no");
return sseService.subscribe();
}
cd k8s
kubectl apply -f redis-deployment.yaml
kubectl apply -f redis-service.yaml
kubectl apply -f sse-example-deploy.yaml
kubectl apply -f sse-example-service.yaml
kubectl apply -f sse-example-ingress-sse.yaml
kubectl apply -f sse-example-ingress-api.yaml
127.0.0.1 sse.ythwork.com
curl -N http://sse.ythwork.com/subscribe
curl -X POST http://sse.ythwork.com/notifications/broadcast \
-H "Content-Type: application/json" \
-d '{"message":"hello"}'
public class BroadcastMessage {
private String eventName;
private String message;
private Long timestamp;
private String senderInstanceId;
// ...
}
@RestController
public class SseController {
// ...
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
return sseService.subscribe();
}
}
@Service
public class SseService {
// ...
public SseEmitter subscribe() {
// ...
SseEmitter emitter = new SseEmitter(timeout);
// ..
try {
emitter.send(SseEmitter.event()
.name("connect")
.data("connected: " + clientId));
} catch (IOException | IllegalStateException ex) {
// ...
}
return emitter;
}
// ...
}
@RestController
@RequestMapping("/notifications")
public class NotificationController {
// ...
@PostMapping("/broadcast")
public String broadcast(@RequestBody NotificationRequest request) {
BroadcastMessage message = new BroadcastMessage(
"notification",
request.getMessage(),
System.currentTimeMillis(),
"server-" + port
);
redisPubSubService.publish(message);
return "published. local connected clients = " + sseService.count();
}
// ...
}
@Service
public class RedisPubSubService {
// ...
public void publish(BroadcastMessage message) {
try {
String payload = objectMapper.writeValueAsString(message);
stringRedisTemplate.convertAndSend(channel, payload);
} catch (JsonProcessingException e) {
// ...
}
}
}
@Component
public class RedisSubscriber implements MessageListener {
// ...
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String body = new String(message.getBody());
BroadcastMessage broadcastMessage =
objectMapper.readValue(body, BroadcastMessage.class);
// ...
sseService.broadcastLocal(
broadcastMessage.getEventName(),
broadcastMessage
);
} catch (Exception e) {
// ...
}
}
}
@Service
public class SseService {
// ...
public void broadcastLocal(String eventName, Object data) {
// ...
repository.findAll().forEach((clientId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data));
} catch (IOException | IllegalStateException ex) {
// ...
}
});
// ...
}
}