>source

Google App Engine에서 실행 중인 NextJS Typescript 앱이 있습니다. Firestore에서 데이터를 가져오고 모든 것이 잘 작동합니다. 앱의 속도를 향상시키기 위해 서버가 Firestore 컬렉션을 수신하고 Firestore에서 변경 사항이 있을 때 모든 데이터를 tmp 폴더의 JSON 파일로 업데이트하는 새로운 데이터 가져오기 인프라를 실험하고 있습니다. 이렇게 하면 모든 데이터가 최신 상태로 유지되며 App Engine에서 항상 사용할 수 있습니다. 로컬에서 이것은 매력처럼 작동합니다.

개선해야 할 몇 가지 분명한 사항이 있지만 다음 단계는 GCP에서 개발 프로젝트를 실행하고 메모리 사용량이 괜찮은지, 원하는 만큼 빨리 작동하는지 확인하는 것입니다. 하지만 문제는 커스텀 서버를 포함하도록 NextJS 인프라를 변경하면 App Engine과 Firestore 간의 연결이 사라집니다.

GCP 로그에서 보고 있는 문제는 다음과 같습니다.

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:180:19)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at runNextTicks (node:internal/process/task_queues:65:3)
    at listOnTimeout (node:internal/timers:526:9)
    at processTimers (node:internal/timers:500:7)
    at async GoogleAuth.getClient (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:558:17)
    at async GrpcClient._getCredentials (/workspace/node_modules/google-gax/build/src/grpc.js:145:24)
    at async GrpcClient.createStub (/workspace/node_modules/google-gax/build/src/grpc.js:308:23)

클라이언트의 실제 오류 메시지는 "502 Bad Gateway – nginx"입니다.

이전에는 프론트엔드 페이지와 백엔드 API 경로가 있는 기본 NextJS 앱이 있었습니다. 경로는 Firestore에 연결하고 올바른 사용자 등을 위해 해당 데이터를 제공합니다. 주요 차이점은 수신기를 시작하는 사용자 지정 서버를 추가했다는 것입니다.

import { Firestore } from '@google-cloud/firestore';
import express, { Request, Response } from 'express';
import next from 'next';
import fs from 'fs';
import os from 'os';
const dev= process.env.NODE_ENV !== 'production';
const app= next({ dev });
const handle= app.getRequestHandler();
const port= process.env.PORT || 3000;
let firestoreListeners: { [collectionId: string]: ()=> void }= {};
const unsubscribeAllListeners= ()=> {
    for (const collectionId of Object.keys(firestoreListeners)) {
        console.log('unsubscribing from', collectionId);
        firestoreListeners[collectionId]();
    }
    firestoreListeners= {};
};
const skippedCollections= ['analytics', 'pageRevisions', 'newsItemRevisions'];
app.prepare().then(()=> {
    const server= express();
    unsubscribeAllListeners();
    const firestoreSettings= {} as FirebaseFirestore.Settings;
    if (process.env.GCP_KEYFILE_NAME) {
        firestoreSettings.keyFilename= process.env.GCP_KEYFILE_NAME;
    }
    const firestoreData: {
        [collectionId: string]: {
            [id: string]: any;
        };
    }= {};
    const firestore= new Firestore(firestoreSettings);
    firestore.listCollections().then((collections)=> {
        for (const collection of collections) {
            if (
                !firestoreListeners[collection.id] &
&
                !skippedCollections.includes(collection.id)
            ) {
                console.log('listening to', collection.id);
                firestoreData[collection.id]= {};
                const listener= firestore
                    .collection(collection.id)
                    .onSnapshot((snapshot)=> {
                        firestoreData[collection.id]= {};
                        for (const doc of snapshot.docs) {
                            firestoreData[collection.id][doc.id]= {
                                _id: doc.id,
                                ...doc.data(),
                            };
                        }
                        if (!fs.existsSync(os.tmpdir() + '/data')) {
                            fs.mkdirSync(os.tmpdir() + '/data');
                        }
                        fs.writeFileSync(
                            os.tmpdir() + `/data/${collection.id}.json`,
                            JSON.stringify(firestoreData[collection.id])
                        );
                        console.log(
                            'updated',
                            collection.id,
                            'with',
                            snapshot.docs.length,
                            'docs'
                        );
                    });
                firestoreListeners[collection.id]= listener;
            }
        }
    });
    server.all('*', (req: Request, res: Response)=> {
        return handle(req, res);
    });
    server.listen(port, (err?: any)=> {
        if (err) throw err;
        console.log(
            `> Ready on localhost:${port} -env ${process.env.NODE_ENV}`
        );
    });
    server.on('close', function () {
        unsubscribeAllListeners();
    });
    process.on('beforeExit', ()=> {
        unsubscribeAllListeners();
    });
});

빌드 및 배포 스크립트는 괜찮습니다. 리스너 논리를 공식에서 제외하고 사용자 지정 서버를 배포하면 작동합니다.

무엇이 문제입니까? nginx 문제입니까 아니면 다른 것이 있습니까?

리스너 논리를 /_ah/warmup 경로로 이동하면 작동하지만 모든 인스턴스에서 시작되지만 인스턴스당 리스너가 하나만 있는지 확인하는 것은 매우 어렵습니다. 그러나 핵심은 아마도 NextJS API 라우트의 리스너 로직과 함께 작동하지만 서버에서 리스너 로직을 완료한 경우에는 작동하지 않는다는 것입니다.

M. Hav2022-02-15 14:06:13

커뮤니티에서 도움이 될 수 있도록 댓글을 답변으로 게시해 주세요.

Robert G2022-02-15 14:06:13

제 댓글이 정말 답이 아닌 것 같습니다.

M. Hav2022-02-15 14:06:13
  • 답변 # 1

    문제는 분명히 이전에 Firestore 연결을 시작할 수 없다는 것입니다.듣다또는 심지어듣다콜백. 조금 후에 해야 합니다(GAE가 Firestore에 대해 인증할 수 있도록 하기 위해?).

    모든 엔드포인트를 수신하도록 리스너를 이동했을 때 작동했습니다. 아래는 문제를 해결한 솔루션입니다. 나는 그것이 그렇게 아름답지는 않지만 일을 끝내고 있다고 생각합니다.

    import { Firestore } from '@google-cloud/firestore';
    import express, { Request, Response } from 'express';
    import next from 'next';
    import fs from 'fs';
    import os from 'os';
    const dev= process.env.NODE_ENV !== 'production';
    const app= next({ dev });
    const handle= app.getRequestHandler();
    const port= process.env.PORT || 3000;
    let firestoreListeners: { [collectionId: string]: ()=> void }= {};
    const unsubscribeAllListeners= ()=> {
        for (const collectionId of Object.keys(firestoreListeners)) {
            console.log('unsubscribing from', collectionId);
            firestoreListeners[collectionId]();
        }
        firestoreListeners= {};
    };
    const skippedCollections= ['analytics', 'pageRevisions', 'newsItemRevisions'];
    export const firestoreData: {
        [collectionId: string]: {
            [id: string]: any;
        };
    }= {};
    let listenersInitiated= false;
    const initiateListeners= ()=> {
        if (listenersInitiated) {
            return;
        }
        const firestoreSettings= {} as FirebaseFirestore.Settings;
        if (process.env.GCP_KEYFILE_NAME) {
            firestoreSettings.keyFilename= process.env.GCP_KEYFILE_NAME;
        }
        const firestore= new Firestore(firestoreSettings);
        firestore.listCollections().then((collections)=> {
            for (const collection of collections) {
                if (
                    !firestoreListeners[collection.id] &
    &
                    !skippedCollections.includes(collection.id)
                ) {
                    console.log('listening to', collection.id);
                    firestoreData[collection.id]= {};
                    const listener= firestore
                        .collection(collection.id)
                        .onSnapshot((snapshot)=> {
                            firestoreData[collection.id]= {};
                            for (const doc of snapshot.docs) {
                                firestoreData[collection.id][doc.id]= {
                                    _id: doc.id,
                                    ...doc.data(),
                                };
                            }
                            if (!fs.existsSync(os.tmpdir() + '/data')) {
                                fs.mkdirSync(os.tmpdir() + '/data');
                            }
                            fs.writeFileSync(
                                os.tmpdir() + `/data/${collection.id}.json`,
                                JSON.stringify(firestoreData[collection.id])
                            );
                            console.log(
                                'updated',
                                collection.id,
                                'with',
                                snapshot.docs.length,
                                'docs'
                            );
                        });
                    firestoreListeners[collection.id]= listener;
                }
            }
        });
        listenersInitiated= true;
    };
    app.prepare().then(()=> {
        const server= express();
        unsubscribeAllListeners();
        server.all('*', (req: Request, res: Response)=> {
            initiateListeners();
            return handle(req, res);
        });
        server.listen(port, (err?: any)=> {
            if (err) throw err;
            console.log(
                `> Ready on localhost:${port} -env ${process.env.NODE_ENV}`
            );
        });
        server.on('close', function () {
            console.log('Closing');
            unsubscribeAllListeners();
        });
        process.on('beforeExit', ()=> {
            console.log('Closing');
            unsubscribeAllListeners();
        });
    });
    

    내 초기 테스트에 따르면 이것은 GAE에서 매우 잘 작동합니다. app.yaml 설정을 올바르게 설정하면 저렴한 비용으로 좋은 속도를 제공합니다.

    이것은 서버 인스턴스가 오랫동안 지속되는 경우 실패하는 리스너를 실제로 처리하지 않으며 너무 많은 리스너를 시작할 수 있지만 테스트의 초기 결과는 유망합니다!

  • 이전 java : jzy3d 빌더는 Eclipse에서 오류를 해결할 수 없습니다.
  • 다음 python : for 루프가 내 데이터 프레임 값을 함수에서 반환된 NaN으로 변경한 이유는 무엇입니까?